duty_free 1.0.4 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
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
  - - ">="