duty_free 1.0.5 → 1.0.6

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: 51bb54397528578ae7d4d43a04d1d6c31f81d1417d9c076f1fb751b18c09afaa
4
- data.tar.gz: 8561d19c0a0166462c011f6a310429944de9b95751dbf9b36dbbb32fdce4bf76
3
+ metadata.gz: f9ac922dd807fc18539ff130c1ccf63412e837acbe4eba0127423235c11d0f1e
4
+ data.tar.gz: e5c27fe4a58718dcbbd4c5ebc1aed2dbb5e7e4b386c977e0ec31269cfad08d48
5
5
  SHA512:
6
- metadata.gz: 02ae8681512885e5ab618aaef343ae88375338fc396ec711b28db78bd97daad361949b86c18c1ca81401e3a15f6629814900a751145bf8a270e50f1fa5669cab
7
- data.tar.gz: 0444426a5468f3dcf7143f3f8859f79f2808db968b6a59561ff0351223993b0071aa94e95b4e50924d99ccbf4ae78f7a9b190a617eac6c21305d8ae0e7dd7f87
6
+ metadata.gz: 54811a68bd34fd5bb881fa19006a7297a8697a3a1575b7060ba7d82e6fcd6d36f545e1d5cfcfc51c756a5b87880e445c415924d2f54aa39471f0b884f0b4c6c4
7
+ data.tar.gz: 32ccb88e429e64bccb59c7741955b99537c661d6416d6f8c352cb384df728081d5b54079cb2dbd904203fbcf1c60abb1a4a7c70bee9c2764fa1622eaaeb6843f
@@ -1,5 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_record/version'
4
+
5
+ # ActiveRecord before 4.0 didn't have #version
6
+ unless ActiveRecord.respond_to?(:version)
7
+ module ActiveRecord
8
+ def self.version
9
+ ::Gem::Version.new(ActiveRecord::VERSION::STRING)
10
+ end
11
+ end
12
+ end
13
+
14
+ # In ActiveSupport older than 5.0, the duplicable? test tries to new up a BigDecimal,
15
+ # and Ruby 2.6 and later deprecates #new. This removes the warning from BigDecimal.
16
+ require 'bigdecimal'
17
+ if ActiveRecord.version < ::Gem::Version.new('5.0') &&
18
+ ::Gem::Version.new(RUBY_VERSION) >= ::Gem::Version.new('2.6')
19
+ def BigDecimal.new(*args, **kwargs)
20
+ BigDecimal(*args, **kwargs)
21
+ end
22
+ end
23
+
24
+ # Allow Rails 4.0 and 4.1 to work with newer Ruby (>= 2.4) by avoiding a "stack level too deep" error
25
+ # when ActiveSupport tries to smarten up Numeric by messing with Fixnum and Bignum at the end of:
26
+ # activesupport-4.0.13/lib/active_support/core_ext/numeric/conversions.rb
27
+ if ActiveRecord.version < ::Gem::Version.new('4.2') &&
28
+ ActiveRecord.version > ::Gem::Version.new('3.2') &&
29
+ Object.const_defined?('Integer') && Integer.superclass.name == 'Numeric'
30
+ class OurFixnum < Integer; end
31
+ Numeric.const_set('Fixnum', OurFixnum)
32
+ class OurBignum < Integer; end
33
+ Numeric.const_set('Bignum', OurBignum)
34
+ end
35
+
36
+ # Allow Rails < 3.2 to run with newer versions of Psych gem
37
+ if BigDecimal.respond_to?(:yaml_tag) && !BigDecimal.respond_to?(:yaml_as)
38
+ class BigDecimal
39
+ class <<self
40
+ alias yaml_as yaml_tag
41
+ end
42
+ end
43
+ end
44
+
3
45
  require 'active_record'
4
46
 
5
47
  require 'duty_free/config'
@@ -65,15 +107,6 @@ end
65
107
  # Major compatibility fixes for ActiveRecord < 4.2
66
108
  # ================================================
67
109
  ActiveSupport.on_load(:active_record) do
68
- # ActiveRecord before 4.0 didn't have #version
69
- unless ActiveRecord.respond_to?(:version)
70
- module ActiveRecord
71
- def self.version
72
- ::Gem::Version.new(ActiveRecord::VERSION::STRING)
73
- end
74
- end
75
- end
76
-
77
110
  # Rails < 4.0 cannot do #find_by, or do #pluck on multiple columns, so here are the patches:
78
111
  if ActiveRecord.version < ::Gem::Version.new('4.0')
79
112
  module ActiveRecord
@@ -97,7 +130,18 @@ ActiveSupport.on_load(:active_record) do
97
130
  else
98
131
  relation = clone # spawn
99
132
  relation.select_values = column_names
100
- result = if respond_to?(:bind_values)
133
+ result = if klass.connection.class.name.end_with?('::PostgreSQLAdapter')
134
+ rslt = klass.connection.execute(relation.arel.to_sql)
135
+ rslt.type_map =
136
+ @type_map ||= proc do
137
+ # This aliasing avoids the warning:
138
+ # "no type cast defined for type "numeric" with oid 1700. Please cast this type
139
+ # explicitly to TEXT to be safe for future changes."
140
+ PG::BasicTypeRegistry.alias_type(0, 'numeric', 'text')
141
+ PG::BasicTypeMapForResults.new(klass.connection.raw_connection)
142
+ end.call
143
+ rslt.to_a
144
+ elsif respond_to?(:bind_values)
101
145
  klass.connection.select_all(relation.arel, nil, bind_values)
102
146
  else
103
147
  klass.connection.select_all(relation.arel.to_sql, nil)
@@ -193,6 +237,13 @@ ActiveSupport.on_load(:active_record) do
193
237
  end
194
238
  end
195
239
 
240
+ if ActiveRecord.version < ::Gem::Version.new('5.0')
241
+ # Avoid pg gem deprecation warning: "You should use PG::Connection, PG::Result, and PG::Error instead"
242
+ PGconn = PG::Connection
243
+ PGresult = PG::Result
244
+ PGError = PG::Error
245
+ end
246
+
196
247
  include ::DutyFree::Extensions
197
248
  end
198
249
 
@@ -51,8 +51,10 @@ module DutyFree
51
51
  rows = [rows]
52
52
 
53
53
  if is_with_data
54
+ order_by = []
55
+ order_by << ['_', primary_key] if primary_key
54
56
  # Automatically create a JOINs strategy and select list to get back all related rows
55
- template_cols, template_joins = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template)
57
+ template_cols, template_joins = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template, order_by)
56
58
  relation = use_inner_joins ? joins(template_joins) : left_joins(template_joins)
57
59
 
58
60
  # So we can properly create the SELECT list, create a mapping between our
@@ -68,7 +70,9 @@ module DutyFree
68
70
  end
69
71
  our_names = ['_'] + ::DutyFree::Util._recurse_arel(template_joins)
70
72
  mapping = our_names.zip(arel_alias_names).to_h
71
-
73
+ relation = (order_by.empty? ? relation : relation.order(order_by.map { |o| "#{mapping[o.first]}.#{o.last}" }))
74
+ # puts mapping.inspect
75
+ # puts relation.dup.select(template_cols.map { |x| x.to_s(mapping) }).to_sql
72
76
  relation.select(template_cols.map { |x| x.to_s(mapping) }).each do |result|
73
77
  rows << ::DutyFree::Extensions._template_columns(self, import_template).map do |col|
74
78
  value = result.send(col)
@@ -217,20 +221,20 @@ module DutyFree
217
221
  is_insert = false
218
222
  existing_unique = valid_unique.inject([]) do |s, v|
219
223
  s << if v.last.is_a?(Array)
220
- # binding.pry
221
224
  v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck(MAX_ID).first.to_s
222
225
  else
223
226
  row[v.last].to_s
224
227
  end
225
228
  end
229
+ to_be_saved = []
226
230
  # Check to see if they want to preprocess anything
227
231
  existing_unique = @before_process.call(valid_unique, existing_unique) if @before_process ||= import_template[:before_process]
228
- obj = if existing.include?(existing_unique)
229
- find(existing[existing_unique])
230
- else
231
- is_insert = true
232
- new
233
- end
232
+ if (criteria = existing[existing_unique])
233
+ obj = find(criteria)
234
+ else
235
+ is_insert = true
236
+ to_be_saved << [obj = new]
237
+ end
234
238
  sub_obj = nil
235
239
  is_has_one = false
236
240
  has_ones = []
@@ -240,28 +244,13 @@ module DutyFree
240
244
  keepers.each do |key, v|
241
245
  next if v.nil?
242
246
 
243
- # Not the same as the last path?
244
- if this_path && v.path != this_path.split(',').map(&:to_sym) && !is_has_one
245
- # puts sub_obj.class.name
246
- if respond_to?(:around_import_save)
247
- # Send them the sub_obj even if it might be invalid so they can choose
248
- # to make it valid if they wish.
249
- # binding.pry
250
- around_import_save(sub_obj) do |modded_obj = nil|
251
- modded_obj = (modded_obj || sub_obj)
252
- modded_obj.save if sub_obj&.valid?
253
- end
254
- elsif sub_obj&.valid?
255
- sub_obj.save
256
- end
257
- end
258
247
  sub_obj = obj
259
248
  this_path = +''
260
249
  # puts "p: #{v.path}"
261
250
  v.path.each_with_index do |path_part, idx|
262
251
  this_path << (this_path.blank? ? path_part.to_s : ",#{path_part}")
263
252
  unless (sub_next = sub_objects[this_path])
264
- # Check if we're hitting platform data / a lookup thing
253
+ # Check if we're hitting reference data / a lookup thing
265
254
  assoc = v.prefix_assocs[idx]
266
255
  # belongs_to some lookup (reference) data
267
256
  if assoc && reference_models.include?(assoc.class_name)
@@ -273,44 +262,33 @@ module DutyFree
273
262
  lookup_match = (lookup_match.length == 1 ? lookup_match.first : nil)
274
263
  end
275
264
  sub_obj.send("#{path_part}=", lookup_match) unless lookup_match.nil?
276
- # Reference data from the platform level means we stop here
265
+ # Reference data from the public level means we stop here
277
266
  sub_obj = nil
278
267
  break
279
268
  end
280
- # This works for belongs_to or has_one. has_many gets sorted below.
281
269
  # Get existing related object, or create a new one
282
- if (sub_next = sub_obj.send(path_part)).nil?
270
+ # This first part works for belongs_to. has_many and has_one get sorted below.
271
+ if (sub_next = sub_obj.send(path_part)).nil? && assoc.belongs_to?
283
272
  klass = Object.const_get(assoc&.class_name)
284
- # assoc.is_a?(ActiveRecord::Reflection::HasOneReflection)
285
- # %%% When we support only AR 4.2 and above then we can do: assoc.has_one?
286
- sub_next = if assoc.macro == :has_one
287
- has_ones << v.path
288
- klass.new
289
- else
290
- # Try to find a unique item if one is referenced
291
- sub_bt = nil
292
- begin
293
- trim_prefix = v.titleize[0..-(v.name.length + 2)]
294
- trim_prefix << ' ' unless trim_prefix.blank?
295
- sub_bt, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, nil, row, klass, all, trim_prefix)
296
- rescue ::DutyFree::NoUniqueColumnError
297
- end
298
- # %%% Can criteria really ever be nil anymore?
299
- sub_bt ||= klass.new(criteria || {}) unless klass == sub_obj.class && criteria.empty?
300
- sub_obj.send("#{path_part}=", sub_bt)
301
- sub_bt
302
- end
303
- end
304
- # Look for possible missing polymorphic detail
305
- # Maybe can test for this via assoc.through_reflection
306
- if assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) &&
307
- (delegate = assoc.send(:delegate_reflection)&.active_record&.reflect_on_association(assoc.source_reflection_name)) &&
308
- delegate.options[:polymorphic]
309
- polymorphics << { parent: sub_next, child: sub_obj, type_col: delegate.foreign_type, id_col: delegate.foreign_key.to_s }
310
- end
311
- # From a has_many?
273
+ # Try to find a unique item if one is referenced
274
+ sub_next = nil
275
+ begin
276
+ trim_prefix = v.titleize[0..-(v.name.length + 2)]
277
+ trim_prefix << ' ' unless trim_prefix.blank?
278
+ sub_next, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, nil, row, klass, all, trim_prefix)
279
+ rescue ::DutyFree::NoUniqueColumnError
280
+ end
281
+ # puts "#{v.path} #{criteria.inspect}"
282
+ bt_name = "#{path_part}="
283
+ unless sub_next || (klass == sub_obj.class && criteria.empty?)
284
+ sub_next = klass.new(criteria || {})
285
+ to_be_saved << [sub_next, sub_obj, bt_name, sub_next]
286
+ end
287
+ sub_obj.send(bt_name, sub_next)
288
+ # From a has_many or has_one?
312
289
  # Rails 4.0 and later can do: sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
313
- if assoc.macro == :has_many && !assoc.options[:through]
290
+ elsif [:has_many, :has_one].include?(assoc.macro) && !assoc.options[:through]
291
+ ::DutyFree::Extensions._save_pending(to_be_saved)
314
292
  # Try to find a unique item if one is referenced
315
293
  # %%% There is possibility that when bringing in related classes using a nil
316
294
  # in IMPORT_TEMPLATE[:all] that this will break. Need to test deeply nested things.
@@ -319,10 +297,33 @@ module DutyFree
319
297
  trim_prefix << ' ' unless trim_prefix.blank?
320
298
  # assoc.inverse_of is the belongs_to side of the has_many train we came in here on.
321
299
  sub_hm, criteria = assoc.klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
322
-
323
300
  # If still not found then create a new related object using this has_many collection
324
301
  # (criteria.empty? ? nil : sub_next.new(criteria))
325
- sub_next = sub_hm || sub_next.new(criteria)
302
+ if sub_hm
303
+ sub_next = sub_hm
304
+ elsif assoc.macro == :has_one
305
+ bt_name = "#{assoc.inverse.name}="
306
+ sub_next = assoc.klass.new(criteria)
307
+ to_be_saved << [sub_next, sub_obj, bt_name, sub_next]
308
+ else
309
+ # Two other methods that are possible to check for here are :conditions and
310
+ # :sanitized_conditions, which do not exist in Rails 4.0 and later.
311
+ sub_next = if assoc.respond_to?(:require_association)
312
+ # With Rails < 4.0, sub_next could come in as an Array or a broken CollectionProxy
313
+ assoc.klass.new({ fk_from(assoc) => sub_obj.send(sub_obj.class.primary_key) }.merge(criteria))
314
+ else
315
+ sub_next.proxy_association.reflection.instance_variable_set(:@foreign_key, fk_from(assoc))
316
+ sub_next.new(criteria)
317
+ end
318
+ to_be_saved << [sub_next]
319
+ end
320
+ end
321
+ # Look for possible missing polymorphic detail
322
+ # Maybe can test for this via assoc.through_reflection
323
+ if assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) &&
324
+ (delegate = assoc.send(:delegate_reflection)&.active_record&.reflect_on_association(assoc.source_reflection_name)) &&
325
+ delegate.options[:polymorphic]
326
+ polymorphics << { parent: sub_next, child: sub_obj, type_col: delegate.foreign_type, id_col: delegate.foreign_key.to_s }
326
327
  end
327
328
  unless sub_next.nil?
328
329
  # if sub_next.class.name == devise_class && # only for Devise users
@@ -336,7 +337,7 @@ module DutyFree
336
337
  sub_objects[this_path] = sub_next if this_path.present?
337
338
  end
338
339
  end
339
- sub_obj = sub_next # if sub_next
340
+ sub_obj = sub_next
340
341
  end
341
342
  next if sub_obj.nil?
342
343
 
@@ -363,15 +364,16 @@ module DutyFree
363
364
  # else
364
365
  # puts " #{sub_obj.class.name} doesn't respond to #{sym}"
365
366
  end
367
+ ::DutyFree::Extensions._save_pending(to_be_saved)
366
368
  # Try to save a final sub-object if one exists
367
- sub_obj.save if sub_obj && this_path && !is_has_one && sub_obj.valid?
369
+ # sub_obj.save if sub_obj && this_path && !is_has_one && sub_obj.valid?
368
370
 
369
- # Wire up has_one associations
370
- has_ones.each do |hasone|
371
- parent = sub_objects[hasone[0..-2].map(&:to_s).join(',')] || obj
372
- hasone_object = sub_objects[hasone.map(&:to_s).join(',')]
373
- parent.send("#{hasone[-1]}=", hasone_object) if parent.new_record? || hasone_object.valid?
374
- end
371
+ # # Wire up has_one associations
372
+ # has_ones.each do |hasone|
373
+ # parent = sub_objects[hasone[0..-2].map(&:to_s).join(',')] || obj
374
+ # hasone_object = sub_objects[hasone.map(&:to_s).join(',')]
375
+ # parent.send("#{hasone[-1]}=", hasone_object) if parent.new_record? || hasone_object.valid?
376
+ # end
375
377
 
376
378
  # Reinstate any missing polymorphic _type and _id values
377
379
  polymorphics.each do |poly|
@@ -425,11 +427,46 @@ module DutyFree
425
427
  ret
426
428
  end
427
429
 
430
+ def fk_from(assoc)
431
+ # Try first to trust whatever they've marked as being the foreign_key, and then look
432
+ # at the inverse's foreign key setting if available. In all cases don't accept
433
+ # anything that's not backed with a real column in the table.
434
+ col_names = assoc.klass.column_names
435
+ if (fk_name = assoc.options[:foreign_key]) && col_names.include?(fk_name.to_s)
436
+ return fk_name
437
+ end
438
+ if (fk_name = assoc.inverse_of&.options&.fetch(:foreign_key) { nil }) &&
439
+ col_names.include?(fk_name.to_s)
440
+ return fk_name
441
+ end
442
+ if assoc.respond_to?(:foreign_key) && (fk_name = assoc.foreign_key) &&
443
+ col_names.include?(fk_name.to_s)
444
+ return fk_name
445
+ end
446
+ if (fk_name = assoc.inverse_of&.foreign_key) &&
447
+ col_names.include?(fk_name.to_s)
448
+ return fk_name
449
+ end
450
+ if (fk_name = assoc.inverse_of&.association_foreign_key) &&
451
+ col_names.include?(fk_name.to_s)
452
+ return fk_name
453
+ end
454
+ # Don't let this fool you -- we really are in search of the foreign key name here,
455
+ # and Rails 3.0 and older used some fairly interesting conventions, calling it instead
456
+ # the "primary_key_name"!
457
+ if assoc.respond_to?(:primary_key_name) && (fk_name = assoc.primary_key_name) &&
458
+ col_names.include?(fk_name.to_s)
459
+ return fk_name
460
+ end
461
+
462
+ puts "* Wow, had no luck at all finding a foreign key for #{assoc.inspect}"
463
+ end
464
+
428
465
  # For use with importing, based on the provided column list calculate all valid combinations
429
466
  # of unique columns. If there is no valid combination, throws an error.
430
467
  # Returns an object found by this means.
431
468
  def find_existing(uniques, cols, starred, import_template, keepers, train_we_came_in_here_on,
432
- row = nil, klass_or_collection = nil, all = nil, trim_prefix = '')
469
+ row = nil, klass_or_collection = nil, template_all = nil, trim_prefix = '')
433
470
  unless trim_prefix.blank?
434
471
  cols = cols.map { |c| c.start_with?(trim_prefix) ? c[trim_prefix.length..-1] : nil }
435
472
  starred = starred.each_with_object([]) do |v, s|
@@ -451,11 +488,16 @@ module DutyFree
451
488
 
452
489
  vus = ((@valid_uniques ||= {})[col_list] ||= {}) # Fancy memoisation
453
490
 
491
+ # First, get an overall list of AVAILABLE COLUMNS before considering tricky foreign key stuff.
492
+ # ============================================================================================
493
+ # Generate a list of column names matched up with their zero-ordinal column number mapping for
494
+ # all columns from the incoming import data.
454
495
  if (is_new_vus = vus.empty?)
455
- # # Let's do general attributes before the tricky foreign key stuff
456
- # Find all unique combinations that are available based on incoming columns, and
457
- # pair them up with column number mappings.
458
- template_column_objects = ::DutyFree::Extensions._recurse_def(self, all || import_template[:all], import_template).first
496
+ template_column_objects = ::DutyFree::Extensions._recurse_def(
497
+ self,
498
+ template_all || import_template[:all],
499
+ import_template
500
+ ).first
459
501
  available = if trim_prefix.blank?
460
502
  template_column_objects.select { |col| col.pre_prefix.blank? && col.prefix.blank? }
461
503
  else
@@ -465,15 +507,18 @@ module DutyFree
465
507
  trim_prefix_snake == "#{this_prefix}_"
466
508
  end
467
509
  end.map { |avail| avail.name.to_s.titleize }
468
- all_vus = defined_uniques(uniques, cols, nil, starred, trim_prefix)
469
- # k, v = all_vus.first
470
- # k.each_with_index do |col, idx|
471
- # if available.include?(col) # || available_bts.include?(col)
472
- # vus[col] ||= v[idx]
473
- # end
474
- # # if available_bts.include?(k)
475
510
  end
476
511
 
512
+ # Process FOREIGN KEY stuff by going through each belongs_to in this model.
513
+ # =========================================================================
514
+ # This list of all valid uniques will help to filter which foreign keys are kept, and also
515
+ # get further filtered later to arrive upon a final set of valid uniques. (Often but not
516
+ # necessarily a specific valid unique as perhaps with a list of users you want to update some
517
+ # folks based on having their email as a unique identifier, and other folks by having a
518
+ # combination of their name and street address as unique, and use both of those possible
519
+ # unique variations to update phone numbers, and do that all as a part of one import.)
520
+ all_vus = defined_uniques(uniques, cols, col_list, starred, trim_prefix)
521
+
477
522
  # %%% Ultimately may consider making this recursive
478
523
  reflect_on_all_associations.each do |sn_bt|
479
524
  next unless sn_bt.belongs_to? && (!train_we_came_in_here_on || sn_bt != train_we_came_in_here_on)
@@ -488,7 +533,7 @@ module DutyFree
488
533
 
489
534
  available_bts << bt_col
490
535
  fk_id = if row
491
- # Max ID so if there are multiple, only the most recent one is picked.
536
+ # Max ID so if there are multiple matches, only the most recent one is picked.
492
537
  # %%% Need to stack these up in case there are multiple
493
538
  # (like first_name, last_name on a referenced employee)
494
539
  sn_bt.klass.where(keepers[idx].name => row[idx]).limit(1).pluck(MAX_ID).first
@@ -519,15 +564,25 @@ module DutyFree
519
564
  bt_col_indexes << idx
520
565
  bt_criteria_all_nil = false
521
566
  end
567
+ # If we're processing a row then this list of foreign key column name entries, named such as
568
+ # "order_id" or "product_id" instead of column-specific stuff like "Order Date" and "Product Name",
569
+ # is kept until the last and then gets merged on top of the other criteria before being returned.
522
570
  bt_criteria[(fk_name = sn_bt.foreign_key)] = fk_id
523
571
 
524
- # Add to our criteria if this belongs_to is required
572
+ # Check to see if belongs_tos are generally required on this specific table
525
573
  bt_req_by_default = sn_bt.klass.respond_to?(:belongs_to_required_by_default) &&
526
574
  sn_bt.klass.belongs_to_required_by_default
527
575
 
528
- # The first check, "!all_vus.keys.first.exists { |k| k.start_with?(bt_prefix) }"
576
+ # Add to our CRITERIA just the belongs_to things that check out.
577
+ # ==============================================================
578
+ # The first check, "all_vus.keys.first.none? { |k| k.start_with?(bt_prefix) }"
529
579
  # is to see if one of the columns we're working with from the unique that we've chosen
530
580
  # comes from the table referenced by this belongs_to (sn_bt).
581
+ #
582
+ # The second check on the :optional option and bt_req_by_default comes directly from
583
+ # how Rails 5 and later checks to see if a specific belongs_to is marked optional
584
+ # (or required), and without having that indication will fall back on checking the model
585
+ # itself to see if it requires belongs_tos by default.
531
586
  next if all_vus.keys.first.none? { |k| k.start_with?(bt_prefix) } &&
532
587
  (sn_bt.options[:optional] || !bt_req_by_default)
533
588
 
@@ -536,6 +591,8 @@ module DutyFree
536
591
  end
537
592
  end
538
593
 
594
+ # Now circle back find a final list of VALID UNIQUES by re-assessing the list of all valid uniques
595
+ # in relation to the available belongs_tos found in the last foreign key step.
539
596
  if is_new_vus
540
597
  available += available_bts
541
598
  all_vus.each do |k, v|
@@ -585,21 +642,21 @@ module DutyFree
585
642
  end
586
643
  end
587
644
 
588
- # Short-circuiting this to only get back the valid_uniques?
589
- # unless uniq_lookups == criteria
590
- # puts "Compare #{uniq_lookups.inspect}"
591
- # puts "Compare #{criteria.inspect}"
592
- # end
593
645
  return uniq_lookups.merge(criteria) if only_valid_uniques
594
646
 
595
647
  # If there's nothing to match upon then we're out
596
648
  return [nil, {}] if new_criteria_all_nil
597
649
 
598
- # With this criteria, find any matching has_many row we can so we can update it
599
- # First try looking it up through ActiveRecord
650
+ # With this criteria, find any matching has_many row we can so we can update it.
651
+ # First try directly looking it up through ActiveRecord.
600
652
  found_object = klass_or_collection.find_by(criteria)
601
653
  # If not successful, such as when fields are exposed via helper methods instead of being
602
- # real columns in the database tables, try this more intensive routine.
654
+ # real columns in the database tables, try this more intensive approach. This is useful
655
+ # if you had full name kind of data coming in on a spreadsheeet, but in the destination
656
+ # table it's broken out to first_name, middle_name, surname. By writing both full_name
657
+ # and full_name= methods, the importer can check to see if this entry is already there,
658
+ # and put a new row in if not, having one incoming name break out to populate three
659
+ # destination columns.
603
660
  unless found_object || klass_or_collection.is_a?(Array)
604
661
  found_object = klass_or_collection.find do |obj|
605
662
  is_good = true
@@ -612,6 +669,9 @@ module DutyFree
612
669
  is_good
613
670
  end
614
671
  end
672
+ # Standard criteria as well as foreign key column name detail with exact foreign keys
673
+ # that match up to a primary key so that if needed a new related object can be built,
674
+ # complete with all its association detail.
615
675
  [found_object, criteria.merge(bt_criteria)]
616
676
  end
617
677
 
@@ -657,6 +717,32 @@ module DutyFree
657
717
  end
658
718
  end # module ClassMethods
659
719
 
720
+ # Called before building any object linked through a has_one or has_many so that foreign key IDs
721
+ # can be added properly to those new objects. Finally at the end also called to save everything.
722
+ def self._save_pending(to_be_saved)
723
+ while (tbs = to_be_saved.pop)
724
+ ais = (tbs.first.class.respond_to?(:around_import_save) && tbs.first.class.method(:around_import_save)) ||
725
+ (respond_to?(:around_import_save) && method(:around_import_save))
726
+ if ais
727
+ # Send them the sub_obj even if it might be invalid so they can choose
728
+ # to make it valid if they wish.
729
+ ais.call(tbs.first) do |modded_obj = nil|
730
+ modded_obj = (modded_obj || tbs.first)
731
+ modded_obj.save if modded_obj&.valid?
732
+ end
733
+ elsif tbs.first.valid?
734
+ tbs.first.save
735
+ else
736
+ puts "* Unable to save #{tbs.first.inspect}"
737
+ end
738
+ # puts "Save #{tbs.first.class.name} #{tbs.first&.id} #{!tbs.first.new_record?}"
739
+ unless tbs[1].nil? || tbs.first.new_record?
740
+ # puts "Calling #{tbs[1].class.name} #{tbs[1]&.id} .#{tbs[2]} #{tbs[3].class.name} #{tbs[3]&.id}"
741
+ tbs[1].send(tbs[2], tbs[3])
742
+ end
743
+ end
744
+ end
745
+
660
746
  # The snake-cased column alias names used in the query to export data
661
747
  def self._template_columns(klass, import_template = nil)
662
748
  template_detail_columns = klass.instance_variable_get(:@template_detail_columns)
@@ -674,16 +760,21 @@ module DutyFree
674
760
 
675
761
  # Recurse and return two arrays -- one with all columns in sequence, and one a hierarchy of
676
762
  # nested hashes to be used with ActiveRecord's .joins() to facilitate export.
677
- def self._recurse_def(klass, array, import_template, assocs = [], joins = [], pre_prefix = '', prefix = '')
763
+ def self._recurse_def(klass, array, import_template, order_by = [], assocs = [], joins = [], pre_prefix = '', prefix = '')
764
+ prefixes = ::DutyFree::Util._prefix_join([pre_prefix, prefix])
678
765
  # Confirm we can actually navigate through this association
679
766
  prefix_assoc = (assocs.last&.klass || klass).reflect_on_association(prefix) if prefix.present?
680
- assocs = assocs.dup << prefix_assoc unless prefix_assoc.nil?
681
- prefixes = ::DutyFree::Util._prefix_join([pre_prefix, prefix])
767
+ if prefix_assoc
768
+ assocs = assocs.dup << prefix_assoc
769
+ if prefix_assoc.macro == :has_many && (pk = prefix_assoc.active_record.primary_key)
770
+ order_by << ["#{prefixes.tr('.', '_')}_", pk]
771
+ end
772
+ end
682
773
  array = array.inject([]) do |s, col|
683
774
  s + if col.is_a?(Hash)
684
775
  col.inject([]) do |s2, v|
685
776
  joins << { v.first.to_sym => (joins_array = []) }
686
- s2 + _recurse_def(klass, (v.last.is_a?(Array) ? v.last : [v.last]), import_template, assocs, joins_array, prefixes, v.first.to_sym).first
777
+ s2 + _recurse_def(klass, (v.last.is_a?(Array) ? v.last : [v.last]), import_template, order_by, assocs, joins_array, prefixes, v.first.to_sym).first
687
778
  end
688
779
  elsif col.nil?
689
780
  if assocs.empty?
@@ -692,7 +783,7 @@ module DutyFree
692
783
  # Bring in from another class
693
784
  joins << { prefix => (joins_array = []) }
694
785
  # %%% Also bring in uniques and requireds
695
- _recurse_def(klass, assocs.last.klass::IMPORT_TEMPLATE[:all], import_template, assocs, joins_array, prefixes).first
786
+ _recurse_def(klass, assocs.last.klass::IMPORT_TEMPLATE[:all], import_template, order_by, assocs, joins_array, prefixes).first
696
787
  end
697
788
  else
698
789
  [::DutyFree::Column.new(col, pre_prefix, prefix, assocs, klass, import_template[:as])]
@@ -65,7 +65,7 @@ module DutyFree
65
65
  end
66
66
  next if is_through || is_habtm || (!is_belongs_to && !do_has_many) || errored_assocs.include?(assoc)
67
67
 
68
- if is_belongs_to && assoc.polymorphic? # Polymorphic belongs_to?
68
+ if is_belongs_to && assoc.options[:polymorphic] # Polymorphic belongs_to?
69
69
  # Load all models
70
70
  # %%% Note that this works in Rails 5.x, but may not work in Rails 6.0 and later, which uses the Zeitwerk loader by default:
71
71
  Rails.configuration.eager_load_namespaces.select { |ns| ns < Rails::Application }.each(&:eager_load!)
@@ -89,7 +89,7 @@ module DutyFree
89
89
  end
90
90
  else
91
91
  # Is it a polymorphic has_many, which is defined using as: :somethingable ?
92
- is_polymorphic_hm = assoc.inverse_of&.polymorphic?
92
+ is_polymorphic_hm = assoc.inverse_of&.options&.fetch(:polymorphic) { nil }
93
93
  begin
94
94
  # Standard has_one, or has_many, and belongs_to uses assoc.klass.
95
95
  # Also polymorphic belongs_to uses assoc.klass.
@@ -105,9 +105,9 @@ module DutyFree
105
105
  [[[fk], assoc.active_record], assoc_klass]
106
106
  else # has_many or has_one
107
107
  inverse_foreign_keys = is_polymorphic_hm ? [assoc.type, assoc.foreign_key] : [assoc.inverse_of&.foreign_key&.to_s]
108
- puts "* Missing inverse foreign key for #{assoc.inspect}" if inverse_foreign_keys.first.nil?
109
108
  missing_key_columns = inverse_foreign_keys - assoc_klass.columns.map(&:name)
110
109
  if missing_key_columns.empty?
110
+ puts "* Missing inverse foreign key for #{this_klass.name} #{belongs_to_or_has_many} :#{assoc.name}" if inverse_foreign_keys.first.nil?
111
111
  # puts "Has columns #{inverse_foreign_keys.inspect}"
112
112
  [[inverse_foreign_keys, assoc_klass], assoc_klass]
113
113
  else
@@ -185,7 +185,7 @@ module DutyFree
185
185
  next unless bt_assoc.belongs_to? && !errored_assocs.include?(bt_assoc)
186
186
 
187
187
  begin
188
- s << bt_assoc if !bt_assoc.polymorphic? && bt_assoc.klass == to_klass
188
+ s << bt_assoc if !bt_assoc.options[:polymorphic] && bt_assoc.klass == to_klass
189
189
  rescue NameError
190
190
  errored_assocs << bt_assoc
191
191
  puts "* In the #{bt_assoc.active_record.name} model \"belongs_to :#{bt_assoc.name}\" could not find a model named #{bt_assoc.class_name}."
@@ -45,7 +45,7 @@ module DutyFree
45
45
  # The right side here at the top is the very last table, and anywhere else down the tree it is
46
46
  # the later "JOIN" table of this pair. (The table that comes after all the rest of the JOINs
47
47
  # from the left side.)
48
- names << piece.right.name
48
+ names << (piece.right.table_alias || piece.right.name)
49
49
  else # "Normal" setup, fed from a JoinSource which has an array of JOINs
50
50
  # The left side is the "JOIN" table
51
51
  names += _recurse_arel(piece.left)
@@ -53,14 +53,14 @@ module DutyFree
53
53
  end
54
54
  # rubocop:enable Style/IdenticalConditionalBranches
55
55
  elsif piece.is_a?(Arel::Table) # Table
56
- names << piece.name
56
+ names << (piece.table_alias || piece.name)
57
57
  elsif piece.is_a?(Arel::Nodes::TableAlias) # Alias
58
58
  # Can get the real table name from: self._recurse_arel(piece.left)
59
59
  names << piece.right.to_s # This is simply a string; the alias name itself
60
60
  elsif piece.is_a?(Arel::Nodes::JoinSource) # Leaving this until the end because AR < 3.2 doesn't know at all about JoinSource!
61
61
  # The left side is the "FROM" table
62
62
  # names += _recurse_arel(piece.left)
63
- names << piece.left.name
63
+ names << (piece.left.table_alias || piece.left.name)
64
64
  # The right side is an array of all JOINs
65
65
  names += piece.right.inject([]) { |s, v| s + _recurse_arel(v) }
66
66
  end
@@ -5,7 +5,7 @@ module DutyFree
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 5
8
+ TINY = 6
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
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.5
4
+ version: 1.0.6
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-20 00:00:00.000000000 Z
11
+ date: 2020-11-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord