duty_free 1.0.4 → 1.0.5

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: f6139333a59c0d3cfb29003ee8d076214906b49270f05de439a260dcf6db227b
4
- data.tar.gz: 2e129dec49e960970b8116983d7b483fa0144511d315586f12a15a2df3ca52b7
3
+ metadata.gz: 51bb54397528578ae7d4d43a04d1d6c31f81d1417d9c076f1fb751b18c09afaa
4
+ data.tar.gz: 8561d19c0a0166462c011f6a310429944de9b95751dbf9b36dbbb32fdce4bf76
5
5
  SHA512:
6
- metadata.gz: 553fe2590a1803cf52b166290b6c2d2525f58d59a45bba5f1b2f1e1a8e6be42451f5e6642c1971195816bdbe8a3573f5327f5b1f9fedf46abe57762bc1131fd4
7
- data.tar.gz: 1ded83dc81de148793c4e98ad809e31efff083b650cd69af07b99e305f0ada3e1a4ccc749a00569838397d018ff1817d40a8a2e89a7ae0964cc8ea4afe4f8ef6
6
+ metadata.gz: 02ae8681512885e5ab618aaef343ae88375338fc396ec711b28db78bd97daad361949b86c18c1ca81401e3a15f6629814900a751145bf8a270e50f1fa5669cab
7
+ data.tar.gz: 0444426a5468f3dcf7143f3f8859f79f2808db968b6a59561ff0351223993b0071aa94e95b4e50924d99ccbf4ae78f7a9b190a617eac6c21305d8ae0e7dd7f87
@@ -62,7 +62,137 @@ module DutyFree
62
62
  end
63
63
  end
64
64
 
65
+ # Major compatibility fixes for ActiveRecord < 4.2
66
+ # ================================================
65
67
  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
+ # Rails < 4.0 cannot do #find_by, or do #pluck on multiple columns, so here are the patches:
78
+ if ActiveRecord.version < ::Gem::Version.new('4.0')
79
+ module ActiveRecord
80
+ module Calculations # Normally find_by is in FinderMethods, which older AR doesn't have
81
+ def find_by(*args)
82
+ where(*args).limit(1).to_a.first
83
+ end
84
+
85
+ def pluck(*column_names)
86
+ column_names.map! do |column_name|
87
+ if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s)
88
+ "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
89
+ else
90
+ column_name
91
+ end
92
+ end
93
+
94
+ # Same as: if has_include?(column_names.first)
95
+ if eager_loading? || (includes_values.present? && (column_names.first || references_eager_loaded_tables?))
96
+ construct_relation_for_association_calculations.pluck(*column_names)
97
+ else
98
+ relation = clone # spawn
99
+ relation.select_values = column_names
100
+ result = if respond_to?(:bind_values)
101
+ klass.connection.select_all(relation.arel, nil, bind_values)
102
+ else
103
+ klass.connection.select_all(relation.arel.to_sql, nil)
104
+ end
105
+ if result.empty?
106
+ []
107
+ else
108
+ columns = result.first.keys.map do |key|
109
+ # rubocop:disable Style/SingleLineMethods Naming/MethodParameterName
110
+ klass.columns_hash.fetch(key) do
111
+ Class.new { def type_cast(v); v; end }.new
112
+ end
113
+ # rubocop:enable Style/SingleLineMethods Naming/MethodParameterName
114
+ end
115
+
116
+ result = result.map do |attributes|
117
+ values = klass.initialize_attributes(attributes).values
118
+
119
+ columns.zip(values).map do |column, value|
120
+ column.type_cast(value)
121
+ end
122
+ end
123
+ columns.one? ? result.map!(&:first) : result
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ unless Base.is_a?(Calculations)
130
+ class Base
131
+ class << self
132
+ delegate :pluck, :find_by, to: :scoped
133
+ end
134
+ end
135
+ end
136
+
137
+ # ActiveRecord < 3.2 doesn't have initialize_attributes, used by .pluck()
138
+ unless AttributeMethods.const_defined?('Serialization')
139
+ class Base
140
+ class << self
141
+ def initialize_attributes(attributes, options = {}) #:nodoc:
142
+ serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
143
+ # super(attributes, options)
144
+
145
+ serialized_attributes.each do |key, coder|
146
+ attributes[key] = Attribute.new(coder, attributes[key], serialized) if attributes.key?(key)
147
+ end
148
+
149
+ attributes
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ # This only gets added for ActiveRecord < 3.2
156
+ module Reflection
157
+ unless AssociationReflection.instance_methods.include?(:foreign_key)
158
+ class AssociationReflection < MacroReflection
159
+ alias foreign_key association_foreign_key
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ # Rails < 4.2 is not innately compatible with Ruby 2.4 and later, and comes up with:
167
+ # "TypeError: Cannot visit Integer" unless we patch like this:
168
+ unless ::Gem::Version.new(RUBY_VERSION) < ::Gem::Version.new('2.4')
169
+ unless Arel::Visitors::DepthFirst.private_instance_methods.include?(:visit_Integer)
170
+ module Arel
171
+ module Visitors
172
+ class DepthFirst < Visitor
173
+ alias visit_Integer terminal
174
+ end
175
+
176
+ class Dot < Visitor
177
+ alias visit_Integer visit_String
178
+ end
179
+
180
+ class ToSql < Visitor
181
+ private
182
+
183
+ # ActiveRecord before v3.2 uses Arel < 3.x, which does not have Arel#literal.
184
+ unless private_instance_methods.include?(:literal)
185
+ def literal(obj)
186
+ obj
187
+ end
188
+ end
189
+ alias visit_Integer literal
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+
66
196
  include ::DutyFree::Extensions
67
197
  end
68
198
 
@@ -36,19 +36,15 @@ module DutyFree
36
36
  end
37
37
 
38
38
  def titleize
39
- @titleized ||= sym_string.titleize
39
+ @titleized ||= to_sym.titleize
40
40
  end
41
41
 
42
- delegate :to_sym, to: :sym_string
43
-
44
42
  def path
45
43
  @path ||= ::DutyFree::Util._prefix_join([pre_prefix, prefix]).split('.').map(&:to_sym)
46
44
  end
47
45
 
48
- private
49
-
50
46
  # The snake-cased column name to be used for building the full list of template_columns
51
- def sym_string
47
+ def to_sym
52
48
  @sym_string ||= ::DutyFree::Util._prefix_join(
53
49
  [pre_prefix, prefix, ::DutyFree::Util._clean_name(name, import_template_as)],
54
50
  '_'
@@ -19,7 +19,8 @@ module DutyFree
19
19
  # end
20
20
 
21
21
  # Export at least column header, and optionally include all existing data as well
22
- def df_export(is_with_data = true, import_template = nil)
22
+ def df_export(is_with_data = true, import_template = nil, use_inner_joins = false)
23
+ use_inner_joins = true unless respond_to?(:left_joins)
23
24
  # In case they are only supplying the columns hash
24
25
  if is_with_data.is_a?(Hash) && !import_template
25
26
  import_template = is_with_data
@@ -52,12 +53,20 @@ module DutyFree
52
53
  if is_with_data
53
54
  # Automatically create a JOINs strategy and select list to get back all related rows
54
55
  template_cols, template_joins = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template)
55
- relation = left_joins(template_joins)
56
+ relation = use_inner_joins ? joins(template_joins) : left_joins(template_joins)
56
57
 
57
58
  # So we can properly create the SELECT list, create a mapping between our
58
59
  # column alias prefixes and the aliases AREL creates.
59
- arel_alias_names = ::DutyFree::Util._recurse_arel(relation.arel.ast.cores.first.source)
60
- our_names = ::DutyFree::Util._recurse_arel(template_joins)
60
+ core = relation.arel.ast.cores.first
61
+ # Accommodate AR < 3.2
62
+ arel_alias_names = if core.froms.is_a?(Arel::Table)
63
+ # All recent versions of AR have #source which brings up an Arel::Nodes::JoinSource
64
+ ::DutyFree::Util._recurse_arel(core.source)
65
+ else
66
+ # With AR < 3.2, "froms" brings up the top node, an Arel::Nodes::InnerJoin
67
+ ::DutyFree::Util._recurse_arel(core.froms)
68
+ end
69
+ our_names = ['_'] + ::DutyFree::Util._recurse_arel(template_joins)
61
70
  mapping = our_names.zip(arel_alias_names).to_h
62
71
 
63
72
  relation.select(template_cols.map { |x| x.to_s(mapping) }).each do |result|
@@ -157,7 +166,6 @@ module DutyFree
157
166
  arguments = [data, import_template][0..last_arg_idx]
158
167
  data = ret if (ret = my_before_import.call(*arguments)).is_a?(Enumerable)
159
168
  end
160
- col_list = nil
161
169
  data.each_with_index do |row, row_num|
162
170
  row_errors = {}
163
171
  if is_first # Anticipate that first row has column names
@@ -186,10 +194,7 @@ module DutyFree
186
194
  col.strip!
187
195
  end
188
196
  end
189
- # %%% Will the uniques saved into @defined_uniques here just get redefined later
190
- # after the next line, the map! with clean to change out the alias names? So we can't yet set
191
- # col_list?
192
- cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) } # %%%
197
+ cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) }
193
198
  defined_uniques(uniques, cols, cols.join('|'), starred)
194
199
  # Make sure that at least half of them match what we know as being good column names
195
200
  template_column_objects = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template).first
@@ -210,9 +215,9 @@ module DutyFree
210
215
  is_first = false
211
216
  else # Normal row of data
212
217
  is_insert = false
213
- is_do_save = true
214
218
  existing_unique = valid_unique.inject([]) do |s, v|
215
219
  s << if v.last.is_a?(Array)
220
+ # binding.pry
216
221
  v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck(MAX_ID).first.to_s
217
222
  else
218
223
  row[v.last].to_s
@@ -252,6 +257,7 @@ module DutyFree
252
257
  end
253
258
  sub_obj = obj
254
259
  this_path = +''
260
+ # puts "p: #{v.path}"
255
261
  v.path.each_with_index do |path_part, idx|
256
262
  this_path << (this_path.blank? ? path_part.to_s : ",#{path_part}")
257
263
  unless (sub_next = sub_objects[this_path])
@@ -274,56 +280,45 @@ module DutyFree
274
280
  # This works for belongs_to or has_one. has_many gets sorted below.
275
281
  # Get existing related object, or create a new one
276
282
  if (sub_next = sub_obj.send(path_part)).nil?
277
- is_has_one = assoc.is_a?(ActiveRecord::Reflection::HasOneReflection)
278
283
  klass = Object.const_get(assoc&.class_name)
279
- sub_next = if is_has_one
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
280
287
  has_ones << v.path
281
288
  klass.new
282
289
  else
283
290
  # Try to find a unique item if one is referenced
284
291
  sub_bt = nil
285
292
  begin
286
- # Goofs up if trim_prefix isn't the same name as the class, or if it's
287
- # a self-join? (like when trim_prefix == 'Reports To')
288
- # %%% Need to test this more when the self-join is more than one hop away,
289
- # such as importing orders and having employees come along :)
290
- # if sub_obj.class == klass
291
- # trim_prefix = ''
292
- # end
293
- # %%% Maybe instead of passing in "klass" we can give the belongs_to association and build through that instead,
294
- # allowing us to nix the klass.new(criteria) line below.
295
293
  trim_prefix = v.titleize[0..-(v.name.length + 2)]
296
294
  trim_prefix << ' ' unless trim_prefix.blank?
297
- # if klass == sub_obj.class # Self-referencing thing pointing to us?
298
- # %%% This should be more general than just for self-referencing things.
299
295
  sub_bt, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, nil, row, klass, all, trim_prefix)
300
296
  rescue ::DutyFree::NoUniqueColumnError
301
- sub_unique = nil
302
297
  end
303
- # Self-referencing shouldn't build a new one if it couldn't find one
304
- # %%% Can criteria really ever be empty anymore?
298
+ # %%% Can criteria really ever be nil anymore?
305
299
  sub_bt ||= klass.new(criteria || {}) unless klass == sub_obj.class && criteria.empty?
306
300
  sub_obj.send("#{path_part}=", sub_bt)
307
301
  sub_bt
308
302
  end
309
303
  end
310
304
  # Look for possible missing polymorphic detail
305
+ # Maybe can test for this via assoc.through_reflection
311
306
  if assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) &&
312
307
  (delegate = assoc.send(:delegate_reflection)&.active_record&.reflect_on_association(assoc.source_reflection_name)) &&
313
308
  delegate.options[:polymorphic]
314
309
  polymorphics << { parent: sub_next, child: sub_obj, type_col: delegate.foreign_type, id_col: delegate.foreign_key.to_s }
315
310
  end
316
311
  # From a has_many?
317
- if sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
312
+ # Rails 4.0 and later can do: sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
313
+ if assoc.macro == :has_many && !assoc.options[:through]
318
314
  # Try to find a unique item if one is referenced
319
315
  # %%% There is possibility that when bringing in related classes using a nil
320
316
  # in IMPORT_TEMPLATE[:all] that this will break. Need to test deeply nested things.
321
317
  start = (v.pre_prefix.blank? ? 0 : v.pre_prefix.length)
322
318
  trim_prefix = v.titleize[start..-(v.name.length + 2)]
323
319
  trim_prefix << ' ' unless trim_prefix.blank?
324
- klass = sub_next.klass
325
320
  # assoc.inverse_of is the belongs_to side of the has_many train we came in here on.
326
- sub_hm, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
321
+ sub_hm, criteria = assoc.klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
327
322
 
328
323
  # If still not found then create a new related object using this has_many collection
329
324
  # (criteria.empty? ? nil : sub_next.new(criteria))
@@ -347,7 +342,7 @@ module DutyFree
347
342
 
348
343
  next unless sub_obj.respond_to?(sym = "#{v.name}=".to_sym)
349
344
 
350
- col_type = (sub_class = sub_obj.class).columns_hash[v.name.to_s]&.type
345
+ col_type = sub_obj.class.columns_hash[v.name.to_s]&.type
351
346
  if col_type.nil? && (virtual_columns = import_template[:virtual_columns]) &&
352
347
  (virtual_columns = virtual_columns[this_path] || virtual_columns)
353
348
  col_type = virtual_columns[v.name]
@@ -355,9 +350,9 @@ module DutyFree
355
350
  if col_type == :boolean
356
351
  if row[key].nil?
357
352
  # Do nothing when it's nil
358
- elsif %w[true t yes y].include?(row[key]&.downcase) # Used to cover 'on'
353
+ elsif %w[true t yes y].include?(row[key]&.strip&.downcase) # Used to cover 'on'
359
354
  row[key] = true
360
- elsif %w[false f no n].include?(row[key]&.downcase) # Used to cover 'off'
355
+ elsif %w[false f no n].include?(row[key]&.strip&.downcase) # Used to cover 'off'
361
356
  row[key] = false
362
357
  else
363
358
  row_errors[v.name] ||= []
@@ -366,7 +361,7 @@ module DutyFree
366
361
  end
367
362
  sub_obj.send(sym, row[key])
368
363
  # else
369
- # puts " #{sub_class.name} doesn't respond to #{sym}"
364
+ # puts " #{sub_obj.class.name} doesn't respond to #{sym}"
370
365
  end
371
366
  # Try to save a final sub-object if one exists
372
367
  sub_obj.save if sub_obj && this_path && !is_has_one && sub_obj.valid?
@@ -442,31 +437,54 @@ module DutyFree
442
437
  s
443
438
  end
444
439
  end
440
+ col_list = cols.join('|')
445
441
 
446
442
  # First add in foreign key stuff we can find from belongs_to associations (other than the
447
443
  # one we might have arrived here upon).
448
- criteria = {}
444
+ criteria = {} # Enough detail to find or build a new object
449
445
  bt_criteria = {}
450
446
  bt_criteria_all_nil = true
451
447
  bt_col_indexes = []
452
448
  available_bts = []
453
449
  only_valid_uniques = (train_we_came_in_here_on == false)
454
- uniq_lookups = {}
455
- # %%% Ultimately may consider making this recursive
456
- bts = reflect_on_all_associations.each_with_object([]) do |sn_assoc, s|
457
- if sn_assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection) &&
458
- (!train_we_came_in_here_on || sn_assoc != train_we_came_in_here_on)
459
- # %%% Make sure there's a starred column we know about from this one
460
- uniq_lookups[sn_assoc.foreign_key] = nil if only_valid_uniques
461
- s << sn_assoc
462
- end
463
- s
450
+ uniq_lookups = {} # The data, or how to look up the data
451
+
452
+ vus = ((@valid_uniques ||= {})[col_list] ||= {}) # Fancy memoisation
453
+
454
+ 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
459
+ available = if trim_prefix.blank?
460
+ template_column_objects.select { |col| col.pre_prefix.blank? && col.prefix.blank? }
461
+ else
462
+ trim_prefix_snake = trim_prefix.downcase.tr(' ', '_')
463
+ template_column_objects.select do |col|
464
+ this_prefix = ::DutyFree::Util._prefix_join([col.pre_prefix, col.prefix], '_').tr('.', '_')
465
+ trim_prefix_snake == "#{this_prefix}_"
466
+ end
467
+ 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)
464
475
  end
465
- bts.each do |sn_bt|
476
+
477
+ # %%% Ultimately may consider making this recursive
478
+ reflect_on_all_associations.each do |sn_bt|
479
+ next unless sn_bt.belongs_to? && (!train_we_came_in_here_on || sn_bt != train_we_came_in_here_on)
480
+
481
+ # # %%% Make sure there's a starred column we know about from this one
482
+ # uniq_lookups[sn_bt.foreign_key] = nil if only_valid_uniques
483
+
466
484
  # This search prefix becomes something like "Order Details Product "
467
485
  cols.each_with_index do |bt_col, idx|
468
486
  next if bt_col_indexes.include?(idx) ||
469
- !bt_col&.start_with?(trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} ")
487
+ !bt_col&.start_with?(bt_prefix = (trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} "))
470
488
 
471
489
  available_bts << bt_col
472
490
  fk_id = if row
@@ -475,6 +493,26 @@ module DutyFree
475
493
  # (like first_name, last_name on a referenced employee)
476
494
  sn_bt.klass.where(keepers[idx].name => row[idx]).limit(1).pluck(MAX_ID).first
477
495
  else
496
+ # elsif is_new_vus
497
+ # # Add to our criteria if this belongs_to is required
498
+ # bt_req_by_default = sn_bt.klass.respond_to?(:belongs_to_required_by_default) &&
499
+ # sn_bt.klass.belongs_to_required_by_default
500
+ # unless !vus.values.first&.include?(idx) &&
501
+ # (sn_bt.options[:optional] || (sn_bt.options[:required] == false) || !bt_req_by_default)
502
+ # # # Add this fk to the criteria
503
+ # # criteria[fk_name] = fk_id
504
+
505
+ # ref = [keepers[idx].name, idx]
506
+ # # bt_criteria[(fk_name = sn_bt.foreign_key)] ||= [sn_bt.klass, []]
507
+ # # bt_criteria[fk_name].last << ref
508
+ # # bt_criteria[bt_col] = [sn_bt.klass, ref]
509
+
510
+ # # Maybe this is the most useful
511
+ # # First array is friendly column names, second is references
512
+ # foreign_uniques = (bt_criteria[sn_bt.name] ||= [sn_bt.klass, [], []])
513
+ # foreign_uniques[1] << ref
514
+ # foreign_uniques[2] << bt_col
515
+ # vus[bt_col] = foreign_uniques # And we can look up this growing set from any foreign column
478
516
  [sn_bt.klass, keepers[idx].name, idx]
479
517
  end
480
518
  if fk_id
@@ -482,74 +520,76 @@ module DutyFree
482
520
  bt_criteria_all_nil = false
483
521
  end
484
522
  bt_criteria[(fk_name = sn_bt.foreign_key)] = fk_id
523
+
485
524
  # Add to our criteria if this belongs_to is required
486
- # %%% Rails older than 5.0 handles this stuff differently!
487
- if sn_bt.options[:optional] || !sn_bt.klass.belongs_to_required_by_default
488
- # Should not have this fk as a requirement
489
- uniq_lookups.delete(fk_name) if only_valid_uniques && uniq_lookups.include?(fk_name)
490
- else # Add to the criteria
491
- criteria[fk_name] = fk_id
492
- end
525
+ bt_req_by_default = sn_bt.klass.respond_to?(:belongs_to_required_by_default) &&
526
+ sn_bt.klass.belongs_to_required_by_default
527
+
528
+ # The first check, "!all_vus.keys.first.exists { |k| k.start_with?(bt_prefix) }"
529
+ # is to see if one of the columns we're working with from the unique that we've chosen
530
+ # comes from the table referenced by this belongs_to (sn_bt).
531
+ next if all_vus.keys.first.none? { |k| k.start_with?(bt_prefix) } &&
532
+ (sn_bt.options[:optional] || !bt_req_by_default)
533
+
534
+ # Add to the criteria
535
+ criteria[fk_name] = fk_id
493
536
  end
494
537
  end
495
538
 
496
- @valid_uniques ||= {} # Fancy memoisation
497
- col_list = cols.join('|')
498
- unless (vus = @valid_uniques[col_list])
499
- # Find all unique combinations that are available based on incoming columns, and
500
- # pair them up with column number mappings.
501
- template_column_objects = ::DutyFree::Extensions._recurse_def(self, all || import_template[:all], import_template).first
502
- available = if trim_prefix.blank?
503
- template_column_objects.select { |col| col.pre_prefix.blank? && col.prefix.blank? }
504
- else
505
- trim_prefix_snake = trim_prefix.downcase.tr(' ', '_')
506
- template_column_objects.select do |col|
507
- this_prefix = ::DutyFree::Util._prefix_join([col.pre_prefix, col.prefix], '_').tr('.', '_')
508
- trim_prefix_snake == "#{this_prefix}_"
509
- end
510
- end.map { |avail| avail.name.to_s.titleize }
511
- vus = defined_uniques(uniques, cols, nil, starred).select do |k, _v|
512
- is_good = true
513
- k.each do |k_col|
514
- unless available.include?(k_col) || available_bts.include?(k_col)
515
- is_good = false
516
- break
539
+ if is_new_vus
540
+ available += available_bts
541
+ all_vus.each do |k, v|
542
+ combined_k = []
543
+ combined_v = []
544
+ k.each_with_index do |key, idx|
545
+ if available.include?(key)
546
+ combined_k << key
547
+ combined_v << v[idx]
517
548
  end
518
549
  end
519
- is_good
550
+ vus[combined_k] = combined_v unless combined_k.empty?
520
551
  end
521
- @valid_uniques[col_list] = vus
522
552
  end
523
553
 
554
+ # uniq_lookups = vus.inject({}) do |s, v|
555
+ # return s if available_bts.include?(v.first) # These will be provided in criteria, and not uniq_lookups
556
+
557
+ # # uniq_lookups[k[trim_prefix.length..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
558
+ # s[v.first.downcase.tr(' ', '_').to_sym] = v.last
559
+ # s
560
+ # end
561
+
562
+ new_criteria_all_nil = bt_criteria_all_nil
563
+
524
564
  # Make sure they have at least one unique combination to take cues from
525
565
  unless vus.empty? # raise NoUniqueColumnError.new(I18n.t('import.no_unique_column_error'))
526
566
  # Convert the first entry to a simplified hash, such as:
527
567
  # {[:investigator_institutions_name, :investigator_institutions_email] => [8, 9], ...}
528
568
  # to {:name => 8, :email => 9}
529
- key, val = vus.first
569
+ key, val = vus.first # Utilise the first identified set of valid uniques
530
570
  key.each_with_index do |k, idx|
531
571
  next if available_bts.include?(k) # These will be provided in criteria, and not uniq_lookups
532
572
 
533
573
  # uniq_lookups[k[trim_prefix.length..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
534
- uniq_lookups[k.downcase.tr(' ', '_').to_sym] = val[idx]
535
- end
536
- end
537
-
538
- # Find by all corresponding columns
574
+ k_sym = k.downcase.tr(' ', '_').to_sym
575
+ v = val[idx]
576
+ uniq_lookups[k_sym] = v # The column number in which to find the data
539
577
 
540
- new_criteria_all_nil = bt_criteria_all_nil
541
- unless only_valid_uniques
542
- uniq_lookups.each do |k, v|
543
- next if bt_col_indexes.include?(v)
578
+ next if only_valid_uniques || bt_col_indexes.include?(v)
544
579
 
580
+ # Find by all corresponding columns
545
581
  if (row_value = row[v])
546
582
  new_criteria_all_nil = false
547
- criteria[k.to_sym] = row_value
583
+ criteria[k_sym] = row_value # The data, or how to look up the data
548
584
  end
549
585
  end
550
586
  end
551
587
 
552
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
553
593
  return uniq_lookups.merge(criteria) if only_valid_uniques
554
594
 
555
595
  # If there's nothing to match upon then we're out
@@ -560,35 +600,37 @@ module DutyFree
560
600
  found_object = klass_or_collection.find_by(criteria)
561
601
  # If not successful, such as when fields are exposed via helper methods instead of being
562
602
  # real columns in the database tables, try this more intensive routine.
563
- found_object ||= klass_or_collection.find do |obj|
564
- is_good = true
565
- criteria.each do |k, v|
566
- if obj.send(k).to_s != v.to_s
567
- is_good = false
568
- break
603
+ unless found_object || klass_or_collection.is_a?(Array)
604
+ found_object = klass_or_collection.find do |obj|
605
+ is_good = true
606
+ criteria.each do |k, v|
607
+ if obj.send(k).to_s != v.to_s
608
+ is_good = false
609
+ break
610
+ end
569
611
  end
612
+ is_good
570
613
  end
571
- is_good
572
614
  end
573
615
  [found_object, criteria.merge(bt_criteria)]
574
616
  end
575
617
 
576
618
  private
577
619
 
578
- def defined_uniques(uniques, cols = [], col_list = nil, starred = [])
620
+ def defined_uniques(uniques, cols = [], col_list = nil, starred = [], trim_prefix = '')
579
621
  col_list ||= cols.join('|')
580
- @defined_uniques ||= {}
581
- unless (defined_uniq = @defined_uniques[col_list])
622
+ unless (defined_uniq = (@defined_uniques ||= {})[col_list])
582
623
  utilised = {} # Track columns that have been referenced thusfar
583
624
  defined_uniq = uniques.each_with_object({}) do |unique, s|
584
625
  if unique.is_a?(Array)
585
626
  key = []
586
627
  value = []
587
628
  unique.each do |unique_part|
588
- val = cols.index(unique_part_name = unique_part.to_s.titleize)
589
- next if val.nil?
629
+ val = (unique_part_name = unique_part.to_s.titleize).start_with?(trim_prefix) &&
630
+ cols.index(upn = unique_part_name[trim_prefix.length..-1])
631
+ next unless val
590
632
 
591
- key << unique_part_name
633
+ key << upn
592
634
  value << val
593
635
  end
594
636
  unless key.empty?
@@ -596,12 +638,14 @@ module DutyFree
596
638
  utilised[key] = nil
597
639
  end
598
640
  else
599
- val = cols.index(unique_part_name = unique.to_s.titleize)
600
- unless val.nil?
601
- s[[unique_part_name]] = [val]
602
- utilised[[unique_part_name]] = nil
641
+ val = (unique_name = unique.to_s.titleize).start_with?(trim_prefix) &&
642
+ cols.index(un = unique_name[trim_prefix.length..-1])
643
+ if val
644
+ s[[un]] = [val]
645
+ utilised[[un]] = nil
603
646
  end
604
647
  end
648
+ s
605
649
  end
606
650
  if defined_uniq.empty?
607
651
  (starred - utilised.keys).each { |star| defined_uniq[[star]] = [cols.index(star)] }
@@ -616,8 +660,8 @@ module DutyFree
616
660
  # The snake-cased column alias names used in the query to export data
617
661
  def self._template_columns(klass, import_template = nil)
618
662
  template_detail_columns = klass.instance_variable_get(:@template_detail_columns)
619
- if (template_import_columns = klass.instance_variable_get(:@template_import_columns)) != import_template
620
- klass.instance_variable_set(:@template_import_columns, template_import_columns = import_template)
663
+ if klass.instance_variable_get(:@template_import_columns) != import_template
664
+ klass.instance_variable_set(:@template_import_columns, import_template)
621
665
  klass.instance_variable_set(:@template_detail_columns, (template_detail_columns = nil))
622
666
  end
623
667
  unless template_detail_columns
@@ -639,7 +683,7 @@ module DutyFree
639
683
  s + if col.is_a?(Hash)
640
684
  col.inject([]) do |s2, v|
641
685
  joins << { v.first.to_sym => (joins_array = []) }
642
- s2 += _recurse_def(klass, (v.last.is_a?(Array) ? v.last : [v.last]), import_template, assocs, joins_array, prefixes, v.first.to_sym).first
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
643
687
  end
644
688
  elsif col.nil?
645
689
  if assocs.empty?
@@ -658,9 +702,13 @@ module DutyFree
658
702
  end
659
703
  end # module Extensions
660
704
 
661
- class NoUniqueColumnError < ActiveRecord::RecordNotUnique
705
+ # Rails < 4.0 doesn't have ActiveRecord::RecordNotUnique, so use the more generic ActiveRecord::ActiveRecordError instead
706
+ ar_not_unique_error = ActiveRecord.const_defined?('RecordNotUnique') ? ActiveRecord::RecordNotUnique : ActiveRecord::ActiveRecordError
707
+ class NoUniqueColumnError < ar_not_unique_error
662
708
  end
663
709
 
664
- class LessThanHalfAreMatchingColumnsError < ActiveRecord::RecordInvalid
710
+ # Rails < 4.2 doesn't have ActiveRecord::RecordInvalid, so use the more generic ActiveRecord::ActiveRecordError instead
711
+ ar_invalid_error = ActiveRecord.const_defined?('RecordInvalid') ? ActiveRecord::RecordInvalid : ActiveRecord::ActiveRecordError
712
+ class LessThanHalfAreMatchingColumnsError < ar_invalid_error
665
713
  end
666
714
  end
@@ -40,15 +40,18 @@ module DutyFree
40
40
  assocs = {}
41
41
  this_klass.reflect_on_all_associations.each do |assoc|
42
42
  # PolymorphicReflection AggregateReflection RuntimeReflection
43
- is_belongs_to = assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection)
43
+ is_belongs_to = assoc.belongs_to? # is_a?(ActiveRecord::Reflection::BelongsToReflection)
44
44
  # Figure out if it's belongs_to, has_many, or has_one
45
+ # HasAndBelongsToManyReflection
45
46
  belongs_to_or_has_many =
46
47
  if is_belongs_to
47
48
  'belongs_to'
48
- elsif (is_habtm = assoc.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection))
49
+ elsif (is_habtm = assoc.respond_to?(:macro) ? (assoc.macro == :has_and_belongs_to_many) : assoc.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection))
49
50
  'has_and_belongs_to_many'
51
+ elsif assoc.respond_to?(:macro) ? (assoc.macro == :has_many) : assoc.is_a?(ActiveRecord::Reflection::HasManyReflection)
52
+ 'has_many'
50
53
  else
51
- (assoc.is_a?(ActiveRecord::Reflection::HasManyReflection) ? 'has_many' : 'has_one')
54
+ 'has_one'
52
55
  end
53
56
  # Always process belongs_to, and also process has_one and has_many if do_has_many is chosen.
54
57
  # Skip any HMT or HABTM. (Maybe break out HABTM into a combo HM and BT in the future.)
@@ -178,7 +181,8 @@ module DutyFree
178
181
  # Find belongs_tos for this model to one more more other klasses
179
182
  def self._find_belongs_tos(klass, to_klass, errored_assocs)
180
183
  klass.reflect_on_all_associations.each_with_object([]) do |bt_assoc, s|
181
- next unless bt_assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection) && !errored_assocs.include?(bt_assoc)
184
+ # .is_a?(ActiveRecord::Reflection::BelongsToReflection)
185
+ next unless bt_assoc.belongs_to? && !errored_assocs.include?(bt_assoc)
182
186
 
183
187
  begin
184
188
  s << bt_assoc if !bt_assoc.polymorphic? && bt_assoc.klass == to_klass
@@ -35,20 +35,34 @@ module DutyFree
35
35
  end
36
36
 
37
37
  # ActiveRecord AREL objects
38
- elsif piece.is_a?(Arel::Nodes::JoinSource)
39
- # The left side is the "FROM" table
40
- # names += _recurse_arel(piece.left)
41
- # The right side is an array of all JOINs
42
- names += piece.right.inject([]) { |s, v| s + _recurse_arel(v) }
43
38
  elsif piece.is_a?(Arel::Nodes::Join) # INNER or OUTER JOIN
44
- # The left side is the "JOIN" table
45
- names += _recurse_arel(piece.left)
46
- # (The right side of these is the "ON" clause)
39
+ # rubocop:disable Style/IdenticalConditionalBranches
40
+ if piece.right.is_a?(Arel::Table) # Came in from AR < 3.2?
41
+ # Arel 2.x and older is a little curious because these JOINs work "back to front".
42
+ # The left side here is either another earlier JOIN, or at the end of the whole tree, it is
43
+ # the first table.
44
+ names += _recurse_arel(piece.left)
45
+ # The right side here at the top is the very last table, and anywhere else down the tree it is
46
+ # the later "JOIN" table of this pair. (The table that comes after all the rest of the JOINs
47
+ # from the left side.)
48
+ names << piece.right.name
49
+ else # "Normal" setup, fed from a JoinSource which has an array of JOINs
50
+ # The left side is the "JOIN" table
51
+ names += _recurse_arel(piece.left)
52
+ # (The right side of these is the "ON" clause)
53
+ end
54
+ # rubocop:enable Style/IdenticalConditionalBranches
47
55
  elsif piece.is_a?(Arel::Table) # Table
48
56
  names << piece.name
49
57
  elsif piece.is_a?(Arel::Nodes::TableAlias) # Alias
50
58
  # Can get the real table name from: self._recurse_arel(piece.left)
51
59
  names << piece.right.to_s # This is simply a string; the alias name itself
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
+ # The left side is the "FROM" table
62
+ # names += _recurse_arel(piece.left)
63
+ names << piece.left.name
64
+ # The right side is an array of all JOINs
65
+ names += piece.right.inject([]) { |s, v| s + _recurse_arel(v) }
52
66
  end
53
67
  names
54
68
  end
@@ -5,7 +5,7 @@ module DutyFree
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 4
8
+ TINY = 5
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.4
4
+ version: 1.0.5
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-15 00:00:00.000000000 Z
11
+ date: 2020-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,20 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.2'
19
+ version: '3.0'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '6.0'
22
+ version: '6.1'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '4.2'
29
+ version: '3.0'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '6.0'
32
+ version: '6.1'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: appraisal
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -204,10 +204,9 @@ dependencies:
204
204
  - - "~>"
205
205
  - !ruby/object:Gem::Version
206
206
  version: '1.4'
207
- description: |
208
- An ActiveRecord extension that simplifies importing and exporting of data
209
- stored in one or more models. Source and destination can be CSV, XLS,
210
- XLSX, ODT, HTML tables, or simple Ruby arrays.
207
+ description: 'Simplify data imports and exports with this slick ActiveRecord extension
208
+
209
+ '
211
210
  email: lorint@gmail.com
212
211
  executables: []
213
212
  extensions: []
@@ -243,7 +242,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
243
242
  requirements:
244
243
  - - ">="
245
244
  - !ruby/object:Gem::Version
246
- version: 2.4.0
245
+ version: 2.3.5
247
246
  required_rubygems_version: !ruby/object:Gem::Requirement
248
247
  requirements:
249
248
  - - ">="