duty_free 1.0.1 → 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: 87d07568c8c7a1692981435e098bd8426d17f30cd733ffff01a7b5a4e4fee4a9
4
- data.tar.gz: 1d2ef595a271f40e5489cac67e576b4f85338d43d46bff26c6e5a6d706766a00
3
+ metadata.gz: f9ac922dd807fc18539ff130c1ccf63412e837acbe4eba0127423235c11d0f1e
4
+ data.tar.gz: e5c27fe4a58718dcbbd4c5ebc1aed2dbb5e7e4b386c977e0ec31269cfad08d48
5
5
  SHA512:
6
- metadata.gz: 4bdd6c53f65cb096de1c2da724a735ecee7ceaed464627f69adbb816b3afa20c85a202cce201a7ee5d35e52ae31663c7f8a9c02d47bc11ea7ead86cebfb0972f
7
- data.tar.gz: 4664e2b6e724f3bba469ef1907b82fe25d90761251ff5cabb204bd75e1a64b522ba3a438438a76e5d58e80952870334abb8194d9d0199b6e2addb2143120b5fa
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'
@@ -62,7 +104,146 @@ module DutyFree
62
104
  end
63
105
  end
64
106
 
107
+ # Major compatibility fixes for ActiveRecord < 4.2
108
+ # ================================================
65
109
  ActiveSupport.on_load(:active_record) do
110
+ # Rails < 4.0 cannot do #find_by, or do #pluck on multiple columns, so here are the patches:
111
+ if ActiveRecord.version < ::Gem::Version.new('4.0')
112
+ module ActiveRecord
113
+ module Calculations # Normally find_by is in FinderMethods, which older AR doesn't have
114
+ def find_by(*args)
115
+ where(*args).limit(1).to_a.first
116
+ end
117
+
118
+ def pluck(*column_names)
119
+ column_names.map! do |column_name|
120
+ if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s)
121
+ "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
122
+ else
123
+ column_name
124
+ end
125
+ end
126
+
127
+ # Same as: if has_include?(column_names.first)
128
+ if eager_loading? || (includes_values.present? && (column_names.first || references_eager_loaded_tables?))
129
+ construct_relation_for_association_calculations.pluck(*column_names)
130
+ else
131
+ relation = clone # spawn
132
+ relation.select_values = column_names
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)
145
+ klass.connection.select_all(relation.arel, nil, bind_values)
146
+ else
147
+ klass.connection.select_all(relation.arel.to_sql, nil)
148
+ end
149
+ if result.empty?
150
+ []
151
+ else
152
+ columns = result.first.keys.map do |key|
153
+ # rubocop:disable Style/SingleLineMethods Naming/MethodParameterName
154
+ klass.columns_hash.fetch(key) do
155
+ Class.new { def type_cast(v); v; end }.new
156
+ end
157
+ # rubocop:enable Style/SingleLineMethods Naming/MethodParameterName
158
+ end
159
+
160
+ result = result.map do |attributes|
161
+ values = klass.initialize_attributes(attributes).values
162
+
163
+ columns.zip(values).map do |column, value|
164
+ column.type_cast(value)
165
+ end
166
+ end
167
+ columns.one? ? result.map!(&:first) : result
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ unless Base.is_a?(Calculations)
174
+ class Base
175
+ class << self
176
+ delegate :pluck, :find_by, to: :scoped
177
+ end
178
+ end
179
+ end
180
+
181
+ # ActiveRecord < 3.2 doesn't have initialize_attributes, used by .pluck()
182
+ unless AttributeMethods.const_defined?('Serialization')
183
+ class Base
184
+ class << self
185
+ def initialize_attributes(attributes, options = {}) #:nodoc:
186
+ serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
187
+ # super(attributes, options)
188
+
189
+ serialized_attributes.each do |key, coder|
190
+ attributes[key] = Attribute.new(coder, attributes[key], serialized) if attributes.key?(key)
191
+ end
192
+
193
+ attributes
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ # This only gets added for ActiveRecord < 3.2
200
+ module Reflection
201
+ unless AssociationReflection.instance_methods.include?(:foreign_key)
202
+ class AssociationReflection < MacroReflection
203
+ alias foreign_key association_foreign_key
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
209
+
210
+ # Rails < 4.2 is not innately compatible with Ruby 2.4 and later, and comes up with:
211
+ # "TypeError: Cannot visit Integer" unless we patch like this:
212
+ unless ::Gem::Version.new(RUBY_VERSION) < ::Gem::Version.new('2.4')
213
+ unless Arel::Visitors::DepthFirst.private_instance_methods.include?(:visit_Integer)
214
+ module Arel
215
+ module Visitors
216
+ class DepthFirst < Visitor
217
+ alias visit_Integer terminal
218
+ end
219
+
220
+ class Dot < Visitor
221
+ alias visit_Integer visit_String
222
+ end
223
+
224
+ class ToSql < Visitor
225
+ private
226
+
227
+ # ActiveRecord before v3.2 uses Arel < 3.x, which does not have Arel#literal.
228
+ unless private_instance_methods.include?(:literal)
229
+ def literal(obj)
230
+ obj
231
+ end
232
+ end
233
+ alias visit_Integer literal
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
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
+
66
247
  include ::DutyFree::Extensions
67
248
  end
68
249
 
@@ -28,25 +28,23 @@ module DutyFree
28
28
  # sql_col = ::DutyFree::Util._prefix_join([table_name.compact.join('_'), name])
29
29
 
30
30
  # Foolproof way, using the AREL mapping:
31
- sql_col = ::DutyFree::Util._prefix_join([mapping["#{pre_prefix.tr('.', '_')}_#{prefix}_"], name])
31
+ this_pre_prefix = pre_prefix.tr('.', '_')
32
+ this_pre_prefix << '_' unless pre_prefix.blank?
33
+ sql_col = ::DutyFree::Util._prefix_join([mapping["#{this_pre_prefix}#{prefix}_"], name])
32
34
  sym = to_sym.to_s
33
35
  sql_col == sym ? sql_col : "#{sql_col} AS #{sym}"
34
36
  end
35
37
 
36
38
  def titleize
37
- @titleized ||= sym_string.titleize
39
+ @titleized ||= to_sym.titleize
38
40
  end
39
41
 
40
- delegate :to_sym, to: :sym_string
41
-
42
42
  def path
43
43
  @path ||= ::DutyFree::Util._prefix_join([pre_prefix, prefix]).split('.').map(&:to_sym)
44
44
  end
45
45
 
46
- private
47
-
48
46
  # The snake-cased column name to be used for building the full list of template_columns
49
- def sym_string
47
+ def to_sym
50
48
  @sym_string ||= ::DutyFree::Util._prefix_join(
51
49
  [pre_prefix, prefix, ::DutyFree::Util._clean_name(name, import_template_as)],
52
50
  '_'
@@ -14,11 +14,13 @@ module DutyFree
14
14
 
15
15
  # :nodoc:
16
16
  module ClassMethods
17
+ MAX_ID = Arel.sql('MAX(id)')
17
18
  # def self.extended(model)
18
19
  # end
19
20
 
20
21
  # Export at least column header, and optionally include all existing data as well
21
- 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)
22
24
  # In case they are only supplying the columns hash
23
25
  if is_with_data.is_a?(Hash) && !import_template
24
26
  import_template = is_with_data
@@ -46,20 +48,31 @@ module DutyFree
46
48
  end
47
49
  (is_required ? '* ' : '') + col
48
50
  end
49
- rows = Array(rows)
51
+ rows = [rows]
50
52
 
51
53
  if is_with_data
54
+ order_by = []
55
+ order_by << ['_', primary_key] if primary_key
52
56
  # Automatically create a JOINs strategy and select list to get back all related rows
53
- template_cols, template_joins = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template)
54
- relation = left_joins(template_joins)
57
+ template_cols, template_joins = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template, order_by)
58
+ relation = use_inner_joins ? joins(template_joins) : left_joins(template_joins)
55
59
 
56
60
  # So we can properly create the SELECT list, create a mapping between our
57
61
  # column alias prefixes and the aliases AREL creates.
58
- # Warning: Delegating ast to arel is deprecated and will be removed in Rails 6.0
59
- arel_alias_names = ::DutyFree::Util._recurse_arel(relation.ast.cores.first.source)
60
- our_names = ::DutyFree::Util._recurse_arel(template_joins)
62
+ core = relation.arel.ast.cores.first
63
+ # Accommodate AR < 3.2
64
+ arel_alias_names = if core.froms.is_a?(Arel::Table)
65
+ # All recent versions of AR have #source which brings up an Arel::Nodes::JoinSource
66
+ ::DutyFree::Util._recurse_arel(core.source)
67
+ else
68
+ # With AR < 3.2, "froms" brings up the top node, an Arel::Nodes::InnerJoin
69
+ ::DutyFree::Util._recurse_arel(core.froms)
70
+ end
71
+ our_names = ['_'] + ::DutyFree::Util._recurse_arel(template_joins)
61
72
  mapping = our_names.zip(arel_alias_names).to_h
62
-
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
63
76
  relation.select(template_cols.map { |x| x.to_s(mapping) }).each do |result|
64
77
  rows << ::DutyFree::Extensions._template_columns(self, import_template).map do |col|
65
78
  value = result.send(col)
@@ -79,15 +92,15 @@ module DutyFree
79
92
 
80
93
  # With an array of incoming data, the first row having column names, perform the import
81
94
  def df_import(data, import_template = nil)
82
- self.instance_variable_set(:@defined_uniques, nil)
83
- self.instance_variable_set(:@valid_uniques, nil)
95
+ instance_variable_set(:@defined_uniques, nil)
96
+ instance_variable_set(:@valid_uniques, nil)
84
97
 
85
98
  import_template ||= if constants.include?(:IMPORT_TEMPLATE)
86
99
  self::IMPORT_TEMPLATE
87
100
  else
88
101
  suggest_template(0, false, false)
89
102
  end
90
- puts "Chose #{import_template}"
103
+ # puts "Chose #{import_template}"
91
104
  inserts = []
92
105
  updates = []
93
106
  counts = Hash.new { |h, k| h[k] = [] }
@@ -105,6 +118,7 @@ module DutyFree
105
118
  devise_class = ''
106
119
  ret = nil
107
120
 
121
+ # Multi-tenancy gem Apartment can be used if there are separate schemas per tenant
108
122
  reference_models = if Object.const_defined?('Apartment')
109
123
  Apartment.excluded_models
110
124
  else
@@ -121,10 +135,13 @@ module DutyFree
121
135
 
122
136
  # Did they give us a filename?
123
137
  if data.is_a?(String)
124
- data = if data.length <= 4096 && data.split('\n').length == 1
138
+ # Filenames with full paths can not be longer than 4096 characters, and can not
139
+ # include newline characters
140
+ data = if data.length <= 4096 && !data.index('\n')
125
141
  File.open(data)
126
142
  else
127
- # Hope that other multi-line strings might be CSV data
143
+ # Any multi-line string is likely CSV data
144
+ # %%% Test to see if TAB characters are present on the first line, instead of commas
128
145
  CSV.new(data)
129
146
  end
130
147
  end
@@ -143,10 +160,16 @@ module DutyFree
143
160
  # Will show as just one transaction when using auditing solutions such as PaperTrail
144
161
  ActiveRecord::Base.transaction do
145
162
  # Check to see if they want to do anything before the whole import
146
- if before_import ||= (import_template[:before_import]) # || some generic before_import)
147
- before_import.call(data)
163
+ # First if defined in the import_template, then if there is a method in the class,
164
+ # and finally (not yet implemented) a generic global before_import
165
+ my_before_import = import_template[:before_import]
166
+ my_before_import ||= respond_to?(:before_import) && method(:before_import)
167
+ # my_before_import ||= some generic my_before_import
168
+ if my_before_import
169
+ last_arg_idx = my_before_import.parameters.length - 1
170
+ arguments = [data, import_template][0..last_arg_idx]
171
+ data = ret if (ret = my_before_import.call(*arguments)).is_a?(Enumerable)
148
172
  end
149
- col_list = nil
150
173
  data.each_with_index do |row, row_num|
151
174
  row_errors = {}
152
175
  if is_first # Anticipate that first row has column names
@@ -175,11 +198,8 @@ module DutyFree
175
198
  col.strip!
176
199
  end
177
200
  end
178
- # %%% Will the uniques saved into @defined_uniques here just get redefined later
179
- # after the next line, the map! with clean to change out the alias names? So we can't yet set
180
- # col_list?
201
+ cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) }
181
202
  defined_uniques(uniques, cols, cols.join('|'), starred)
182
- cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) } # %%%
183
203
  # Make sure that at least half of them match what we know as being good column names
184
204
  template_column_objects = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template).first
185
205
  cols.each_with_index do |col, idx|
@@ -187,9 +207,7 @@ module DutyFree
187
207
  keepers[idx] = template_column_objects.find { |col_obj| col_obj.titleize == col }
188
208
  # puts "Could not find a match for column #{idx + 1}, #{col}" if keepers[idx].nil?
189
209
  end
190
- if keepers.length < (cols.length / 2) - 1
191
- raise ::DutyFree::LessThanHalfAreMatchingColumnsError, I18n.t('import.altered_import_template_coumns')
192
- end
210
+ raise ::DutyFree::LessThanHalfAreMatchingColumnsError, I18n.t('import.altered_import_template_coumns') if keepers.length < (cols.length / 2) - 1
193
211
 
194
212
  # Returns just the first valid unique lookup set if there are multiple
195
213
  valid_unique = find_existing(uniques, cols, starred, import_template, keepers, false)
@@ -201,24 +219,22 @@ module DutyFree
201
219
  is_first = false
202
220
  else # Normal row of data
203
221
  is_insert = false
204
- is_do_save = true
205
222
  existing_unique = valid_unique.inject([]) do |s, v|
206
223
  s << if v.last.is_a?(Array)
207
- v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck('MAX(id)').first.to_s
224
+ v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck(MAX_ID).first.to_s
208
225
  else
209
226
  row[v.last].to_s
210
227
  end
211
228
  end
229
+ to_be_saved = []
212
230
  # Check to see if they want to preprocess anything
213
- if @before_process ||= import_template[:before_process]
214
- existing_unique = @before_process.call(valid_unique, existing_unique)
231
+ existing_unique = @before_process.call(valid_unique, existing_unique) if @before_process ||= import_template[:before_process]
232
+ if (criteria = existing[existing_unique])
233
+ obj = find(criteria)
234
+ else
235
+ is_insert = true
236
+ to_be_saved << [obj = new]
215
237
  end
216
- obj = if existing.include?(existing_unique)
217
- find(existing[existing_unique])
218
- else
219
- is_insert = true
220
- new
221
- end
222
238
  sub_obj = nil
223
239
  is_has_one = false
224
240
  has_ones = []
@@ -226,31 +242,15 @@ module DutyFree
226
242
  sub_objects = {}
227
243
  this_path = nil
228
244
  keepers.each do |key, v|
229
- klass = nil
230
245
  next if v.nil?
231
246
 
232
- # Not the same as the last path?
233
- if this_path && v.path != this_path.split(',').map(&:to_sym) && !is_has_one
234
- # puts sub_obj.class.name
235
- if respond_to?(:around_import_save)
236
- # Send them the sub_obj even if it might be invalid so they can choose
237
- # to make it valid if they wish.
238
- # binding.pry
239
- around_import_save(sub_obj) do |modded_obj = nil|
240
- modded_obj = (modded_obj || sub_obj)
241
- modded_obj.save if sub_obj&.valid?
242
- end
243
- elsif sub_obj&.valid?
244
- # binding.pry if sub_obj.is_a?(Employee) && sub_obj.first_name == 'Andrew'
245
- sub_obj.save
246
- end
247
- end
248
247
  sub_obj = obj
249
248
  this_path = +''
249
+ # puts "p: #{v.path}"
250
250
  v.path.each_with_index do |path_part, idx|
251
251
  this_path << (this_path.blank? ? path_part.to_s : ",#{path_part}")
252
252
  unless (sub_next = sub_objects[this_path])
253
- # Check if we're hitting platform data / a lookup thing
253
+ # Check if we're hitting reference data / a lookup thing
254
254
  assoc = v.prefix_assocs[idx]
255
255
  # belongs_to some lookup (reference) data
256
256
  if assoc && reference_models.include?(assoc.class_name)
@@ -262,74 +262,68 @@ module DutyFree
262
262
  lookup_match = (lookup_match.length == 1 ? lookup_match.first : nil)
263
263
  end
264
264
  sub_obj.send("#{path_part}=", lookup_match) unless lookup_match.nil?
265
- # Reference data from the platform level means we stop here
265
+ # Reference data from the public level means we stop here
266
266
  sub_obj = nil
267
267
  break
268
268
  end
269
- # This works for belongs_to or has_one. has_many gets sorted below.
270
269
  # Get existing related object, or create a new one
271
- if (sub_next = sub_obj.send(path_part)).nil?
272
- is_has_one = assoc.is_a?(ActiveRecord::Reflection::HasOneReflection)
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?
273
272
  klass = Object.const_get(assoc&.class_name)
274
- sub_next = if is_has_one
275
- has_ones << v.path
276
- klass.new
277
- else
278
- # Try to find a unique item if one is referenced
279
- sub_bt = nil
280
- begin
281
- # Goofs up if trim_prefix isn't the same name as the class, or if it's
282
- # a self-join? (like when trim_prefix == 'Reports To')
283
- # %%% Need to test this more when the self-join is more than one hop away,
284
- # such as importing orders and having employees come along :)
285
- # if sub_obj.class == klass
286
- # trim_prefix = ''
287
- # # binding.pry
288
- # end
289
- # %%% Maybe instead of passing in "klass" we can give the belongs_to association and build through that instead,
290
- # allowing us to nix the klass.new(criteria) line below.
291
- trim_prefix = v.titleize[0..-(v.name.length + 2)]
292
- trim_prefix << ' ' unless trim_prefix.blank?
293
- if klass == sub_obj.class # Self-referencing thing pointing to us?
294
- # %%% This should be more general than just for self-referencing things.
295
- sub_cols = cols.map { |c| c.start_with?(trim_prefix) ? c[trim_prefix.length..-1] : nil }
296
- sub_bt, criteria = klass.find_existing(uniques, sub_cols, starred, import_template, keepers, assoc, row, klass, all, trim_prefix)
297
- else
298
- sub_bt, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, nil, row, klass, all, trim_prefix)
299
- end
300
- rescue ::DutyFree::NoUniqueColumnError
301
- sub_unique = nil
302
- end
303
- # binding.pry if sub_obj.is_a?(Employee) && sub_obj.first_name == 'Nancy' &&
304
- # sub_bt.is_a?(Employee)
305
- # %%% Can criteria really ever be nil anymore?
306
- sub_bt ||= klass.new(criteria || {})
307
- sub_obj.send("#{path_part}=", sub_bt)
308
- sub_bt # unless klass == sub_obj.class # Don't go further if it's self-referencing
309
- end
310
- end
311
- # Look for possible missing polymorphic detail
312
- if assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) &&
313
- (delegate = assoc.send(:delegate_reflection)&.active_record&.reflect_on_association(assoc.source_reflection_name)) &&
314
- delegate.options[:polymorphic]
315
- polymorphics << { parent: sub_next, child: sub_obj, type_col: delegate.foreign_type, id_col: delegate.foreign_key.to_s }
316
- end
317
- # From a has_many?
318
- if sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
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?
289
+ # Rails 4.0 and later can do: sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
290
+ elsif [:has_many, :has_one].include?(assoc.macro) && !assoc.options[:through]
291
+ ::DutyFree::Extensions._save_pending(to_be_saved)
319
292
  # Try to find a unique item if one is referenced
320
293
  # %%% There is possibility that when bringing in related classes using a nil
321
294
  # in IMPORT_TEMPLATE[:all] that this will break. Need to test deeply nested things.
322
295
  start = (v.pre_prefix.blank? ? 0 : v.pre_prefix.length)
323
296
  trim_prefix = v.titleize[start..-(v.name.length + 2)]
324
297
  trim_prefix << ' ' unless trim_prefix.blank?
325
- klass = sub_next.klass
326
- # binding.pry if klass.name == 'OrderDetail'
327
-
328
298
  # assoc.inverse_of is the belongs_to side of the has_many train we came in here on.
329
- sub_hm, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
330
-
299
+ sub_hm, criteria = assoc.klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
331
300
  # If still not found then create a new related object using this has_many collection
332
- sub_next = sub_hm || sub_next.new(criteria)
301
+ # (criteria.empty? ? nil : 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 }
333
327
  end
334
328
  unless sub_next.nil?
335
329
  # if sub_next.class.name == devise_class && # only for Devise users
@@ -343,14 +337,13 @@ module DutyFree
343
337
  sub_objects[this_path] = sub_next if this_path.present?
344
338
  end
345
339
  end
346
- sub_obj = sub_next unless sub_next.nil?
340
+ sub_obj = sub_next
347
341
  end
348
342
  next if sub_obj.nil?
349
343
 
350
- sym = "#{v.name}=".to_sym
351
- next unless sub_obj.respond_to?(sym)
344
+ next unless sub_obj.respond_to?(sym = "#{v.name}=".to_sym)
352
345
 
353
- col_type = (sub_class = sub_obj.class).columns_hash[v.name.to_s]&.type
346
+ col_type = sub_obj.class.columns_hash[v.name.to_s]&.type
354
347
  if col_type.nil? && (virtual_columns = import_template[:virtual_columns]) &&
355
348
  (virtual_columns = virtual_columns[this_path] || virtual_columns)
356
349
  col_type = virtual_columns[v.name]
@@ -358,9 +351,9 @@ module DutyFree
358
351
  if col_type == :boolean
359
352
  if row[key].nil?
360
353
  # Do nothing when it's nil
361
- elsif %w[yes y].include?(row[key]&.downcase) # Used to cover 'true', 't', 'on'
354
+ elsif %w[true t yes y].include?(row[key]&.strip&.downcase) # Used to cover 'on'
362
355
  row[key] = true
363
- elsif %w[no n].include?(row[key]&.downcase) # Used to cover 'false', 'f', 'off'
356
+ elsif %w[false f no n].include?(row[key]&.strip&.downcase) # Used to cover 'off'
364
357
  row[key] = false
365
358
  else
366
359
  row_errors[v.name] ||= []
@@ -369,17 +362,18 @@ module DutyFree
369
362
  end
370
363
  sub_obj.send(sym, row[key])
371
364
  # else
372
- # puts " #{sub_class.name} doesn't respond to #{sym}"
365
+ # puts " #{sub_obj.class.name} doesn't respond to #{sym}"
373
366
  end
367
+ ::DutyFree::Extensions._save_pending(to_be_saved)
374
368
  # Try to save a final sub-object if one exists
375
- 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?
376
370
 
377
- # Wire up has_one associations
378
- has_ones.each do |hasone|
379
- parent = sub_objects[hasone[0..-2].map(&:to_s).join(',')] || obj
380
- hasone_object = sub_objects[hasone.map(&:to_s).join(',')]
381
- parent.send("#{hasone[-1]}=", hasone_object) if parent.new_record? || hasone_object.valid?
382
- 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
383
377
 
384
378
  # Reinstate any missing polymorphic _type and _id values
385
379
  polymorphics.each do |poly|
@@ -416,26 +410,94 @@ module DutyFree
416
410
  s += v.last[1..-1].map { |line_num| { line_num => v.first } } if v.last.count > 1
417
411
  s
418
412
  end
419
- # Check to see if they want to do anything after the import
420
413
  ret = { inserted: inserts, updated: updates, duplicates: duplicates, errors: errors }
421
- if @after_import ||= (import_template[:after_import]) # || some generic after_import)
422
- ret = ret2 if (ret2 = @after_import.call(ret)).is_a?(Hash)
414
+
415
+ # Check to see if they want to do anything after the import
416
+ # First if defined in the import_template, then if there is a method in the class,
417
+ # and finally (not yet implemented) a generic global after_import
418
+ my_after_import = import_template[:after_import]
419
+ my_after_import ||= respond_to?(:after_import) && method(:after_import)
420
+ # my_after_import ||= some generic my_after_import
421
+ if my_after_import
422
+ last_arg_idx = my_after_import.parameters.length - 1
423
+ arguments = [ret][0..last_arg_idx]
424
+ ret = ret2 if (ret2 = my_after_import.call(*arguments)).is_a?(Hash)
423
425
  end
424
426
  end
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
- def find_existing(uniques, cols, starred, import_template, keepers, train_we_came_in_here_on, row = nil, obj = nil, all = nil, trim_prefix = '')
432
- col_name_offset = trim_prefix.length
433
- @valid_uniques ||= {} # Fancy memoisation
468
+ def find_existing(uniques, cols, starred, import_template, keepers, train_we_came_in_here_on,
469
+ row = nil, klass_or_collection = nil, template_all = nil, trim_prefix = '')
470
+ unless trim_prefix.blank?
471
+ cols = cols.map { |c| c.start_with?(trim_prefix) ? c[trim_prefix.length..-1] : nil }
472
+ starred = starred.each_with_object([]) do |v, s|
473
+ s << v[trim_prefix.length..-1] if v.start_with?(trim_prefix)
474
+ s
475
+ end
476
+ end
434
477
  col_list = cols.join('|')
435
- unless (vus = @valid_uniques[col_list])
436
- # Find all unique combinations that are available based on incoming columns, and
437
- # pair them up with column number mappings.
438
- template_column_objects = ::DutyFree::Extensions._recurse_def(self, all || import_template[:all], import_template).first
478
+
479
+ # First add in foreign key stuff we can find from belongs_to associations (other than the
480
+ # one we might have arrived here upon).
481
+ criteria = {} # Enough detail to find or build a new object
482
+ bt_criteria = {}
483
+ bt_criteria_all_nil = true
484
+ bt_col_indexes = []
485
+ available_bts = []
486
+ only_valid_uniques = (train_we_came_in_here_on == false)
487
+ uniq_lookups = {} # The data, or how to look up the data
488
+
489
+ vus = ((@valid_uniques ||= {})[col_list] ||= {}) # Fancy memoisation
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.
495
+ if (is_new_vus = vus.empty?)
496
+ template_column_objects = ::DutyFree::Extensions._recurse_def(
497
+ self,
498
+ template_all || import_template[:all],
499
+ import_template
500
+ ).first
439
501
  available = if trim_prefix.blank?
440
502
  template_column_objects.select { |col| col.pre_prefix.blank? && col.prefix.blank? }
441
503
  else
@@ -445,113 +507,190 @@ module DutyFree
445
507
  trim_prefix_snake == "#{this_prefix}_"
446
508
  end
447
509
  end.map { |avail| avail.name.to_s.titleize }
448
- vus = defined_uniques(uniques, cols, nil, starred).select do |k, _v|
449
- is_good = true
450
- k.each do |k_col|
451
- unless k_col.start_with?(trim_prefix) && available.include?(k_col[col_name_offset..-1])
452
- is_good = false
453
- break
510
+ end
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
+
522
+ # %%% Ultimately may consider making this recursive
523
+ reflect_on_all_associations.each do |sn_bt|
524
+ next unless sn_bt.belongs_to? && (!train_we_came_in_here_on || sn_bt != train_we_came_in_here_on)
525
+
526
+ # # %%% Make sure there's a starred column we know about from this one
527
+ # uniq_lookups[sn_bt.foreign_key] = nil if only_valid_uniques
528
+
529
+ # This search prefix becomes something like "Order Details Product "
530
+ cols.each_with_index do |bt_col, idx|
531
+ next if bt_col_indexes.include?(idx) ||
532
+ !bt_col&.start_with?(bt_prefix = (trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} "))
533
+
534
+ available_bts << bt_col
535
+ fk_id = if row
536
+ # Max ID so if there are multiple matches, only the most recent one is picked.
537
+ # %%% Need to stack these up in case there are multiple
538
+ # (like first_name, last_name on a referenced employee)
539
+ sn_bt.klass.where(keepers[idx].name => row[idx]).limit(1).pluck(MAX_ID).first
540
+ else
541
+ # elsif is_new_vus
542
+ # # Add to our criteria if this belongs_to is required
543
+ # bt_req_by_default = sn_bt.klass.respond_to?(:belongs_to_required_by_default) &&
544
+ # sn_bt.klass.belongs_to_required_by_default
545
+ # unless !vus.values.first&.include?(idx) &&
546
+ # (sn_bt.options[:optional] || (sn_bt.options[:required] == false) || !bt_req_by_default)
547
+ # # # Add this fk to the criteria
548
+ # # criteria[fk_name] = fk_id
549
+
550
+ # ref = [keepers[idx].name, idx]
551
+ # # bt_criteria[(fk_name = sn_bt.foreign_key)] ||= [sn_bt.klass, []]
552
+ # # bt_criteria[fk_name].last << ref
553
+ # # bt_criteria[bt_col] = [sn_bt.klass, ref]
554
+
555
+ # # Maybe this is the most useful
556
+ # # First array is friendly column names, second is references
557
+ # foreign_uniques = (bt_criteria[sn_bt.name] ||= [sn_bt.klass, [], []])
558
+ # foreign_uniques[1] << ref
559
+ # foreign_uniques[2] << bt_col
560
+ # vus[bt_col] = foreign_uniques # And we can look up this growing set from any foreign column
561
+ [sn_bt.klass, keepers[idx].name, idx]
562
+ end
563
+ if fk_id
564
+ bt_col_indexes << idx
565
+ bt_criteria_all_nil = false
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.
570
+ bt_criteria[(fk_name = sn_bt.foreign_key)] = fk_id
571
+
572
+ # Check to see if belongs_tos are generally required on this specific table
573
+ bt_req_by_default = sn_bt.klass.respond_to?(:belongs_to_required_by_default) &&
574
+ sn_bt.klass.belongs_to_required_by_default
575
+
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) }"
579
+ # is to see if one of the columns we're working with from the unique that we've chosen
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.
586
+ next if all_vus.keys.first.none? { |k| k.start_with?(bt_prefix) } &&
587
+ (sn_bt.options[:optional] || !bt_req_by_default)
588
+
589
+ # Add to the criteria
590
+ criteria[fk_name] = fk_id
591
+ end
592
+ end
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.
596
+ if is_new_vus
597
+ available += available_bts
598
+ all_vus.each do |k, v|
599
+ combined_k = []
600
+ combined_v = []
601
+ k.each_with_index do |key, idx|
602
+ if available.include?(key)
603
+ combined_k << key
604
+ combined_v << v[idx]
454
605
  end
455
606
  end
456
- is_good
607
+ vus[combined_k] = combined_v unless combined_k.empty?
457
608
  end
458
- @valid_uniques[col_list] = vus
459
609
  end
460
610
 
611
+ # uniq_lookups = vus.inject({}) do |s, v|
612
+ # return s if available_bts.include?(v.first) # These will be provided in criteria, and not uniq_lookups
613
+
614
+ # # uniq_lookups[k[trim_prefix.length..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
615
+ # s[v.first.downcase.tr(' ', '_').to_sym] = v.last
616
+ # s
617
+ # end
618
+
619
+ new_criteria_all_nil = bt_criteria_all_nil
620
+
461
621
  # Make sure they have at least one unique combination to take cues from
462
- ret = {}
463
622
  unless vus.empty? # raise NoUniqueColumnError.new(I18n.t('import.no_unique_column_error'))
464
623
  # Convert the first entry to a simplified hash, such as:
465
624
  # {[:investigator_institutions_name, :investigator_institutions_email] => [8, 9], ...}
466
625
  # to {:name => 8, :email => 9}
467
- key, val = vus.first
626
+ key, val = vus.first # Utilise the first identified set of valid uniques
468
627
  key.each_with_index do |k, idx|
469
- ret[k[col_name_offset..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
470
- end
471
- end
628
+ next if available_bts.include?(k) # These will be provided in criteria, and not uniq_lookups
472
629
 
473
- # %%% If uniqueness is based on something else hanging out on a belongs_to then we're pretty hosed.
474
- # (Case in point, importing Order with related Order Detail and Product, and then Product needs to
475
- # be found or built first before OrderDetail.)
476
- # Might have to do a deferred save kind of thing, and also make sure the product stuff came first
477
- # before the other stuff
478
-
479
- # Add in any foreign key stuff we can find from other belongs_to associations
480
- # %%% This is starting to look like the other BelongsToAssociation code above around line
481
- # 697, so it really needs to be turned into something recursive instead of this two-layer
482
- # thick thing at best.
483
- bts = reflect_on_all_associations.each_with_object([]) do |sn_assoc, s|
484
- if sn_assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection) &&
485
- (!train_we_came_in_here_on || sn_assoc != train_we_came_in_here_on) &&
486
- sn_assoc.klass != self # Omit stuff pointing to us (like self-referencing stuff)
487
- # %%% Make sure there's a starred column we know about from this one
488
- ret[sn_assoc.foreign_key] = nil if train_we_came_in_here_on == false
489
- s << sn_assoc
490
- end
491
- s
492
- end
493
-
494
- # Find by all corresponding columns
495
- criteria = {}
496
- if train_we_came_in_here_on != false
497
- criteria = ret.each_with_object({}) do |v, s|
498
- s[v.first.to_sym] = row[v.last]
499
- s
500
- end
501
- end
630
+ # uniq_lookups[k[trim_prefix.length..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
631
+ k_sym = k.downcase.tr(' ', '_').to_sym
632
+ v = val[idx]
633
+ uniq_lookups[k_sym] = v # The column number in which to find the data
502
634
 
503
- bts.each do |sn_bt|
504
- # This search prefix becomes something like "Order Details Product "
505
- cols.each_with_index do |bt_col, idx|
506
- next unless bt_col.start_with?(trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} ")
635
+ next if only_valid_uniques || bt_col_indexes.include?(v)
507
636
 
508
- fk_id = if row
509
- # Max ID so if there are multiple, only the most recent one is picked.
510
- # %%% Need to stack these up in case there are multiple (like first_name, last_name on a referenced employee)
511
- sn_bt.klass.where(keepers[idx].name => row[idx]).limit(1).pluck('MAX(id)').first
512
- else
513
- [sn_bt.klass, keepers[idx].name, idx]
514
- end
515
- criteria[sn_bt.foreign_key] = fk_id
637
+ # Find by all corresponding columns
638
+ if (row_value = row[v])
639
+ new_criteria_all_nil = false
640
+ criteria[k_sym] = row_value # The data, or how to look up the data
641
+ end
516
642
  end
517
643
  end
518
644
 
519
- # Short-circuiting this to only get back the valid_uniques?
520
- return ret.merge(criteria) if train_we_came_in_here_on == false
521
-
522
- # With this criteria, find any matching has_many row we can so we can update it
523
- sub_hm = obj.find do |hm_obj|
524
- is_good = true
525
- criteria.each do |k, v|
526
- if hm_obj.send(k).to_s != v.to_s
527
- is_good = false
528
- break
645
+ return uniq_lookups.merge(criteria) if only_valid_uniques
646
+
647
+ # If there's nothing to match upon then we're out
648
+ return [nil, {}] if new_criteria_all_nil
649
+
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.
652
+ found_object = klass_or_collection.find_by(criteria)
653
+ # If not successful, such as when fields are exposed via helper methods instead of being
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.
660
+ unless found_object || klass_or_collection.is_a?(Array)
661
+ found_object = klass_or_collection.find do |obj|
662
+ is_good = true
663
+ criteria.each do |k, v|
664
+ if obj.send(k).to_s != v.to_s
665
+ is_good = false
666
+ break
667
+ end
529
668
  end
669
+ is_good
530
670
  end
531
- is_good
532
671
  end
533
- # Try looking it up through ActiveRecord
534
- # %%% Should we perhaps do this first before the more intensive find routine above?
535
- sub_hm = obj.find_by(criteria) if sub_hm.nil?
536
- [sub_hm, criteria]
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.
675
+ [found_object, criteria.merge(bt_criteria)]
537
676
  end
538
677
 
539
678
  private
540
679
 
541
- def defined_uniques(uniques, cols = [], col_list = nil, starred = [])
680
+ def defined_uniques(uniques, cols = [], col_list = nil, starred = [], trim_prefix = '')
542
681
  col_list ||= cols.join('|')
543
- @defined_uniques ||= {}
544
- unless (defined_uniq = @defined_uniques[col_list])
682
+ unless (defined_uniq = (@defined_uniques ||= {})[col_list])
545
683
  utilised = {} # Track columns that have been referenced thusfar
546
684
  defined_uniq = uniques.each_with_object({}) do |unique, s|
547
685
  if unique.is_a?(Array)
548
686
  key = []
549
687
  value = []
550
688
  unique.each do |unique_part|
551
- val = cols.index(unique_part_name = unique_part.to_s.titleize)
552
- next if val.nil?
689
+ val = (unique_part_name = unique_part.to_s.titleize).start_with?(trim_prefix) &&
690
+ cols.index(upn = unique_part_name[trim_prefix.length..-1])
691
+ next unless val
553
692
 
554
- key << unique_part_name
693
+ key << upn
555
694
  value << val
556
695
  end
557
696
  unless key.empty?
@@ -559,29 +698,60 @@ module DutyFree
559
698
  utilised[key] = nil
560
699
  end
561
700
  else
562
- val = cols.index(unique_part_name = unique.to_s.titleize)
563
- unless val.nil?
564
- s[[unique_part_name]] = [val]
565
- utilised[[unique_part_name]] = nil
701
+ val = (unique_name = unique.to_s.titleize).start_with?(trim_prefix) &&
702
+ cols.index(un = unique_name[trim_prefix.length..-1])
703
+ if val
704
+ s[[un]] = [val]
705
+ utilised[[un]] = nil
566
706
  end
567
707
  end
708
+ s
709
+ end
710
+ if defined_uniq.empty?
711
+ (starred - utilised.keys).each { |star| defined_uniq[[star]] = [cols.index(star)] }
712
+ # %%% puts "Tried to establish #{defined_uniq.inspect}"
568
713
  end
569
- (starred - utilised.keys).each { |star| defined_uniq[[star]] = [cols.index(star)] }
570
714
  @defined_uniques[col_list] = defined_uniq
571
715
  end
572
716
  defined_uniq
573
717
  end
574
718
  end # module ClassMethods
575
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
+
576
746
  # The snake-cased column alias names used in the query to export data
577
747
  def self._template_columns(klass, import_template = nil)
578
748
  template_detail_columns = klass.instance_variable_get(:@template_detail_columns)
579
- if (template_import_columns = klass.instance_variable_get(:@template_import_columns)) != import_template
580
- klass.instance_variable_set(:@template_import_columns, template_import_columns = import_template)
749
+ if klass.instance_variable_get(:@template_import_columns) != import_template
750
+ klass.instance_variable_set(:@template_import_columns, import_template)
581
751
  klass.instance_variable_set(:@template_detail_columns, (template_detail_columns = nil))
582
752
  end
583
753
  unless template_detail_columns
584
- puts "* Redoing *"
754
+ # puts "* Redoing *"
585
755
  template_detail_columns = _recurse_def(klass, import_template[:all], import_template).first.map(&:to_sym)
586
756
  klass.instance_variable_set(:@template_detail_columns, template_detail_columns)
587
757
  end
@@ -590,16 +760,21 @@ module DutyFree
590
760
 
591
761
  # Recurse and return two arrays -- one with all columns in sequence, and one a hierarchy of
592
762
  # nested hashes to be used with ActiveRecord's .joins() to facilitate export.
593
- def self._recurse_def(klass, array, import_template, assocs = [], joins = [], pre_prefix = '', prefix = '')
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])
594
765
  # Confirm we can actually navigate through this association
595
766
  prefix_assoc = (assocs.last&.klass || klass).reflect_on_association(prefix) if prefix.present?
596
- assocs = assocs.dup << prefix_assoc unless prefix_assoc.nil?
597
- prefixes = ::DutyFree::Util._prefix_join([pre_prefix, prefix])
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
598
773
  array = array.inject([]) do |s, col|
599
774
  s + if col.is_a?(Hash)
600
775
  col.inject([]) do |s2, v|
601
776
  joins << { v.first.to_sym => (joins_array = []) }
602
- s2 += _recurse_def(klass, (v.last.is_a?(Array) ? v.last : [v.last]), import_template, assocs, joins_array, prefixes, v.first.to_sym).first
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
603
778
  end
604
779
  elsif col.nil?
605
780
  if assocs.empty?
@@ -608,7 +783,7 @@ module DutyFree
608
783
  # Bring in from another class
609
784
  joins << { prefix => (joins_array = []) }
610
785
  # %%% Also bring in uniques and requireds
611
- _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
612
787
  end
613
788
  else
614
789
  [::DutyFree::Column.new(col, pre_prefix, prefix, assocs, klass, import_template[:as])]
@@ -618,9 +793,13 @@ module DutyFree
618
793
  end
619
794
  end # module Extensions
620
795
 
621
- class NoUniqueColumnError < ActiveRecord::RecordNotUnique
796
+ # Rails < 4.0 doesn't have ActiveRecord::RecordNotUnique, so use the more generic ActiveRecord::ActiveRecordError instead
797
+ ar_not_unique_error = ActiveRecord.const_defined?('RecordNotUnique') ? ActiveRecord::RecordNotUnique : ActiveRecord::ActiveRecordError
798
+ class NoUniqueColumnError < ar_not_unique_error
622
799
  end
623
800
 
624
- class LessThanHalfAreMatchingColumnsError < ActiveRecord::RecordInvalid
801
+ # Rails < 4.2 doesn't have ActiveRecord::RecordInvalid, so use the more generic ActiveRecord::ActiveRecordError instead
802
+ ar_invalid_error = ActiveRecord.const_defined?('RecordInvalid') ? ActiveRecord::RecordInvalid : ActiveRecord::ActiveRecordError
803
+ class LessThanHalfAreMatchingColumnsError < ar_invalid_error
625
804
  end
626
805
  end