duty_free 1.0.1 → 1.0.6

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: 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