duty_free 1.0.2 → 1.0.7

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: aa22e01d02997c6b8bc562f19805d1f0f529e17793b0dd14282f43840ea2c86c
4
- data.tar.gz: afa848b412ea1514e0afeaa36ba1093ccb1baecdd4384d225b07f76145e9457e
3
+ metadata.gz: b16ccd7258656d274fc6b1f8798c5b5d1f3822570bdd9600f10d55e486a1308d
4
+ data.tar.gz: 9174b6bb8521edfacd9ddde72decc8a5b9274f33049740914cdd5077f9d52c59
5
5
  SHA512:
6
- metadata.gz: c60d6f2a72d34f4f99d3f9fea16b0ed2d1ff71b6088e4b6782a0b54decc15ef5ba45cef17f846079f4e5a55fd2f5dfcf25945b80351ff6006318b59fedcffd13
7
- data.tar.gz: 6be734a732268268ef86768d88215e62b947c97326d823f3b631bab16de3f919877fedbf4184da1e1490904a752ed8e675bb173966cb9f2922860ae0b606020d
6
+ metadata.gz: 8ba849219eca3426c396be95fc29828c9fe60cd4c538004796d9cc9c17d8be11541c1b18f3d6ed0a89e2980570caafe88dbfc9772ca2d48fe17239c5417d71d8
7
+ data.tar.gz: 54fa0c8b8bd4719863ebeb246f1d1fd21decec7ed1061b9ab8b447e2d38216a52ad427e2a3964564d19a6016ee572b6a95d7f269ed409e90df4c96cbbec01910
@@ -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,155 @@ 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') # oid 1700
141
+ PG::BasicTypeRegistry.alias_type(0, 'time', 'text') # oid 1083
142
+ PG::BasicTypeMapForResults.new(klass.connection.raw_connection)
143
+ end.call
144
+ rslt.to_a
145
+ elsif respond_to?(:bind_values)
146
+ klass.connection.select_all(relation.arel, nil, bind_values)
147
+ else
148
+ klass.connection.select_all(relation.arel.to_sql, nil)
149
+ end
150
+ if result.empty?
151
+ []
152
+ else
153
+ columns = result.first.keys.map do |key|
154
+ # rubocop:disable Style/SingleLineMethods Naming/MethodParameterName
155
+ klass.columns_hash.fetch(key) do
156
+ Class.new { def type_cast(v); v; end }.new
157
+ end
158
+ # rubocop:enable Style/SingleLineMethods Naming/MethodParameterName
159
+ end
160
+
161
+ result = result.map do |attributes|
162
+ values = klass.initialize_attributes(attributes).values
163
+
164
+ columns.zip(values).map do |column, value|
165
+ column.type_cast(value)
166
+ end
167
+ end
168
+ columns.one? ? result.map!(&:first) : result
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ unless Base.is_a?(Calculations)
175
+ class Base
176
+ class << self
177
+ delegate :pluck, :find_by, to: :scoped
178
+ end
179
+ end
180
+ end
181
+
182
+ # ActiveRecord < 3.2 doesn't have initialize_attributes, used by .pluck()
183
+ unless AttributeMethods.const_defined?('Serialization')
184
+ class Base
185
+ class << self
186
+ def initialize_attributes(attributes, options = {}) #:nodoc:
187
+ serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
188
+ # super(attributes, options)
189
+
190
+ serialized_attributes.each do |key, coder|
191
+ attributes[key] = Attribute.new(coder, attributes[key], serialized) if attributes.key?(key)
192
+ end
193
+
194
+ attributes
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ # This only gets added for ActiveRecord < 3.2
201
+ module Reflection
202
+ unless AssociationReflection.instance_methods.include?(:foreign_key)
203
+ class AssociationReflection < MacroReflection
204
+ alias foreign_key association_foreign_key
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
210
+
211
+ # Rails < 4.2 is not innately compatible with Ruby 2.4 and later, and comes up with:
212
+ # "TypeError: Cannot visit Integer" unless we patch like this:
213
+ unless ::Gem::Version.new(RUBY_VERSION) < ::Gem::Version.new('2.4')
214
+ unless Arel::Visitors::DepthFirst.private_instance_methods.include?(:visit_Integer)
215
+ module Arel
216
+ module Visitors
217
+ class DepthFirst < Visitor
218
+ alias visit_Integer terminal
219
+ end
220
+
221
+ class Dot < Visitor
222
+ alias visit_Integer visit_String
223
+ end
224
+
225
+ class ToSql < Visitor
226
+ private
227
+
228
+ # ActiveRecord before v3.2 uses Arel < 3.x, which does not have Arel#literal.
229
+ unless private_instance_methods.include?(:literal)
230
+ def literal(obj)
231
+ obj
232
+ end
233
+ end
234
+ alias visit_Integer literal
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ if ActiveRecord.version < ::Gem::Version.new('5.0')
242
+ # Avoid pg gem deprecation warning: "You should use PG::Connection, PG::Result, and PG::Error instead"
243
+ PGconn = PG::Connection
244
+ PGresult = PG::Result
245
+ PGError = PG::Error
246
+ end
247
+
248
+ unless DateTime.instance_methods.include?(:nsec)
249
+ class DateTime < Date
250
+ def nsec
251
+ (sec_fraction * 1000000000).to_i
252
+ end
253
+ end
254
+ end
255
+
66
256
  include ::DutyFree::Extensions
67
257
  end
68
258
 
@@ -36,19 +36,15 @@ module DutyFree
36
36
  end
37
37
 
38
38
  def titleize
39
- @titleized ||= sym_string.titleize
39
+ @titleized ||= to_sym.titleize
40
40
  end
41
41
 
42
- delegate :to_sym, to: :sym_string
43
-
44
42
  def path
45
43
  @path ||= ::DutyFree::Util._prefix_join([pre_prefix, prefix]).split('.').map(&:to_sym)
46
44
  end
47
45
 
48
- private
49
-
50
46
  # The snake-cased column name to be used for building the full list of template_columns
51
- def sym_string
47
+ def to_sym
52
48
  @sym_string ||= ::DutyFree::Util._prefix_join(
53
49
  [pre_prefix, prefix, ::DutyFree::Util._clean_name(name, import_template_as)],
54
50
  '_'
@@ -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
@@ -49,16 +51,31 @@ module DutyFree
49
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
- arel_alias_names = ::DutyFree::Util._recurse_arel(relation.arel.ast.cores.first.source)
59
- our_names = ::DutyFree::Util._recurse_arel(template_joins)
62
+ # %%% If with Rails 3.1 and older you get "NoMethodError: undefined method `eq' for nil:NilClass"
63
+ # when trying to call relation.arel, then somewhere along the line while navigating a has_many
64
+ # relationship it can't find the proper foreign key.
65
+ core = relation.arel.ast.cores.first
66
+ # Accommodate AR < 3.2
67
+ arel_alias_names = if core.froms.is_a?(Arel::Table)
68
+ # All recent versions of AR have #source which brings up an Arel::Nodes::JoinSource
69
+ ::DutyFree::Util._recurse_arel(core.source)
70
+ else
71
+ # With AR < 3.2, "froms" brings up the top node, an Arel::Nodes::InnerJoin
72
+ ::DutyFree::Util._recurse_arel(core.froms)
73
+ end
74
+ our_names = ['_'] + ::DutyFree::Util._recurse_arel(template_joins)
60
75
  mapping = our_names.zip(arel_alias_names).to_h
61
-
76
+ relation = (order_by.empty? ? relation : relation.order(order_by.map { |o| "#{mapping[o.first]}.#{o.last}" }))
77
+ # puts mapping.inspect
78
+ # puts relation.dup.select(template_cols.map { |x| x.to_s(mapping) }).to_sql
62
79
  relation.select(template_cols.map { |x| x.to_s(mapping) }).each do |result|
63
80
  rows << ::DutyFree::Extensions._template_columns(self, import_template).map do |col|
64
81
  value = result.send(col)
@@ -78,8 +95,8 @@ module DutyFree
78
95
 
79
96
  # With an array of incoming data, the first row having column names, perform the import
80
97
  def df_import(data, import_template = nil)
81
- self.instance_variable_set(:@defined_uniques, nil)
82
- self.instance_variable_set(:@valid_uniques, nil)
98
+ instance_variable_set(:@defined_uniques, nil)
99
+ instance_variable_set(:@valid_uniques, nil)
83
100
 
84
101
  import_template ||= if constants.include?(:IMPORT_TEMPLATE)
85
102
  self::IMPORT_TEMPLATE
@@ -104,6 +121,7 @@ module DutyFree
104
121
  devise_class = ''
105
122
  ret = nil
106
123
 
124
+ # Multi-tenancy gem Apartment can be used if there are separate schemas per tenant
107
125
  reference_models = if Object.const_defined?('Apartment')
108
126
  Apartment.excluded_models
109
127
  else
@@ -120,10 +138,13 @@ module DutyFree
120
138
 
121
139
  # Did they give us a filename?
122
140
  if data.is_a?(String)
123
- data = if data.length <= 4096 && data.split('\n').length == 1
141
+ # Filenames with full paths can not be longer than 4096 characters, and can not
142
+ # include newline characters
143
+ data = if data.length <= 4096 && !data.index('\n')
124
144
  File.open(data)
125
145
  else
126
- # Hope that other multi-line strings might be CSV data
146
+ # Any multi-line string is likely CSV data
147
+ # %%% Test to see if TAB characters are present on the first line, instead of commas
127
148
  CSV.new(data)
128
149
  end
129
150
  end
@@ -142,10 +163,16 @@ module DutyFree
142
163
  # Will show as just one transaction when using auditing solutions such as PaperTrail
143
164
  ActiveRecord::Base.transaction do
144
165
  # Check to see if they want to do anything before the whole import
145
- if before_import ||= (import_template[:before_import]) # || some generic before_import)
146
- before_import.call(data)
166
+ # First if defined in the import_template, then if there is a method in the class,
167
+ # and finally (not yet implemented) a generic global before_import
168
+ my_before_import = import_template[:before_import]
169
+ my_before_import ||= respond_to?(:before_import) && method(:before_import)
170
+ # my_before_import ||= some generic my_before_import
171
+ if my_before_import
172
+ last_arg_idx = my_before_import.parameters.length - 1
173
+ arguments = [data, import_template][0..last_arg_idx]
174
+ data = ret if (ret = my_before_import.call(*arguments)).is_a?(Enumerable)
147
175
  end
148
- col_list = nil
149
176
  data.each_with_index do |row, row_num|
150
177
  row_errors = {}
151
178
  if is_first # Anticipate that first row has column names
@@ -174,11 +201,8 @@ module DutyFree
174
201
  col.strip!
175
202
  end
176
203
  end
177
- # %%% Will the uniques saved into @defined_uniques here just get redefined later
178
- # after the next line, the map! with clean to change out the alias names? So we can't yet set
179
- # col_list?
204
+ cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) }
180
205
  defined_uniques(uniques, cols, cols.join('|'), starred)
181
- cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) } # %%%
182
206
  # Make sure that at least half of them match what we know as being good column names
183
207
  template_column_objects = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template).first
184
208
  cols.each_with_index do |col, idx|
@@ -186,9 +210,7 @@ module DutyFree
186
210
  keepers[idx] = template_column_objects.find { |col_obj| col_obj.titleize == col }
187
211
  # puts "Could not find a match for column #{idx + 1}, #{col}" if keepers[idx].nil?
188
212
  end
189
- if keepers.length < (cols.length / 2) - 1
190
- raise ::DutyFree::LessThanHalfAreMatchingColumnsError, I18n.t('import.altered_import_template_coumns')
191
- end
213
+ raise ::DutyFree::LessThanHalfAreMatchingColumnsError, I18n.t('import.altered_import_template_coumns') if keepers.length < (cols.length / 2) - 1
192
214
 
193
215
  # Returns just the first valid unique lookup set if there are multiple
194
216
  valid_unique = find_existing(uniques, cols, starred, import_template, keepers, false)
@@ -200,24 +222,22 @@ module DutyFree
200
222
  is_first = false
201
223
  else # Normal row of data
202
224
  is_insert = false
203
- is_do_save = true
204
225
  existing_unique = valid_unique.inject([]) do |s, v|
205
226
  s << if v.last.is_a?(Array)
206
- v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck('MAX(id)').first.to_s
227
+ v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck(MAX_ID).first.to_s
207
228
  else
208
229
  row[v.last].to_s
209
230
  end
210
231
  end
232
+ to_be_saved = []
211
233
  # Check to see if they want to preprocess anything
212
- if @before_process ||= import_template[:before_process]
213
- existing_unique = @before_process.call(valid_unique, existing_unique)
234
+ existing_unique = @before_process.call(valid_unique, existing_unique) if @before_process ||= import_template[:before_process]
235
+ if (criteria = existing[existing_unique])
236
+ obj = find(criteria)
237
+ else
238
+ is_insert = true
239
+ to_be_saved << [obj = new]
214
240
  end
215
- obj = if existing.include?(existing_unique)
216
- find(existing[existing_unique])
217
- else
218
- is_insert = true
219
- new
220
- end
221
241
  sub_obj = nil
222
242
  is_has_one = false
223
243
  has_ones = []
@@ -225,31 +245,15 @@ module DutyFree
225
245
  sub_objects = {}
226
246
  this_path = nil
227
247
  keepers.each do |key, v|
228
- klass = nil
229
248
  next if v.nil?
230
249
 
231
- # Not the same as the last path?
232
- if this_path && v.path != this_path.split(',').map(&:to_sym) && !is_has_one
233
- # puts sub_obj.class.name
234
- if respond_to?(:around_import_save)
235
- # Send them the sub_obj even if it might be invalid so they can choose
236
- # to make it valid if they wish.
237
- # binding.pry
238
- around_import_save(sub_obj) do |modded_obj = nil|
239
- modded_obj = (modded_obj || sub_obj)
240
- modded_obj.save if sub_obj&.valid?
241
- end
242
- elsif sub_obj&.valid?
243
- # binding.pry if sub_obj.is_a?(Employee) && sub_obj.first_name == 'Andrew'
244
- sub_obj.save
245
- end
246
- end
247
250
  sub_obj = obj
248
251
  this_path = +''
252
+ # puts "p: #{v.path}"
249
253
  v.path.each_with_index do |path_part, idx|
250
254
  this_path << (this_path.blank? ? path_part.to_s : ",#{path_part}")
251
255
  unless (sub_next = sub_objects[this_path])
252
- # Check if we're hitting platform data / a lookup thing
256
+ # Check if we're hitting reference data / a lookup thing
253
257
  assoc = v.prefix_assocs[idx]
254
258
  # belongs_to some lookup (reference) data
255
259
  if assoc && reference_models.include?(assoc.class_name)
@@ -261,74 +265,82 @@ module DutyFree
261
265
  lookup_match = (lookup_match.length == 1 ? lookup_match.first : nil)
262
266
  end
263
267
  sub_obj.send("#{path_part}=", lookup_match) unless lookup_match.nil?
264
- # Reference data from the platform level means we stop here
268
+ # Reference data from the public level means we stop here
265
269
  sub_obj = nil
266
270
  break
267
271
  end
268
- # This works for belongs_to or has_one. has_many gets sorted below.
269
272
  # Get existing related object, or create a new one
270
- if (sub_next = sub_obj.send(path_part)).nil?
271
- is_has_one = assoc.is_a?(ActiveRecord::Reflection::HasOneReflection)
273
+ # This first part works for belongs_to. has_many and has_one get sorted below.
274
+ if (sub_next = sub_obj.send(path_part)).nil? && assoc.belongs_to?
272
275
  klass = Object.const_get(assoc&.class_name)
273
- sub_next = if is_has_one
274
- has_ones << v.path
275
- klass.new
276
- else
277
- # Try to find a unique item if one is referenced
278
- sub_bt = nil
279
- begin
280
- # Goofs up if trim_prefix isn't the same name as the class, or if it's
281
- # a self-join? (like when trim_prefix == 'Reports To')
282
- # %%% Need to test this more when the self-join is more than one hop away,
283
- # such as importing orders and having employees come along :)
284
- # if sub_obj.class == klass
285
- # trim_prefix = ''
286
- # # binding.pry
287
- # end
288
- # %%% Maybe instead of passing in "klass" we can give the belongs_to association and build through that instead,
289
- # allowing us to nix the klass.new(criteria) line below.
290
- trim_prefix = v.titleize[0..-(v.name.length + 2)]
291
- trim_prefix << ' ' unless trim_prefix.blank?
292
- if klass == sub_obj.class # Self-referencing thing pointing to us?
293
- # %%% This should be more general than just for self-referencing things.
294
- sub_cols = cols.map { |c| c.start_with?(trim_prefix) ? c[trim_prefix.length..-1] : nil }
295
- sub_bt, criteria = klass.find_existing(uniques, sub_cols, starred, import_template, keepers, assoc, row, klass, all, trim_prefix)
296
- else
297
- sub_bt, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, nil, row, klass, all, trim_prefix)
298
- end
299
- rescue ::DutyFree::NoUniqueColumnError
300
- sub_unique = nil
301
- end
302
- # binding.pry if sub_obj.is_a?(Employee) && sub_obj.first_name == 'Nancy' &&
303
- # sub_bt.is_a?(Employee)
304
- # %%% Can criteria really ever be nil anymore?
305
- sub_bt ||= klass.new(criteria || {})
306
- sub_obj.send("#{path_part}=", sub_bt)
307
- sub_bt # unless klass == sub_obj.class # Don't go further if it's self-referencing
308
- end
309
- end
310
- # Look for possible missing polymorphic detail
311
- if assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) &&
312
- (delegate = assoc.send(:delegate_reflection)&.active_record&.reflect_on_association(assoc.source_reflection_name)) &&
313
- delegate.options[:polymorphic]
314
- polymorphics << { parent: sub_next, child: sub_obj, type_col: delegate.foreign_type, id_col: delegate.foreign_key.to_s }
315
- end
316
- # From a has_many?
317
- if sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
276
+ # Try to find a unique item if one is referenced
277
+ sub_next = nil
278
+ begin
279
+ trim_prefix = v.titleize[0..-(v.name.length + 2)]
280
+ trim_prefix << ' ' unless trim_prefix.blank?
281
+ sub_next, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, nil, row, klass, all, trim_prefix)
282
+ rescue ::DutyFree::NoUniqueColumnError
283
+ end
284
+ # puts "#{v.path} #{criteria.inspect}"
285
+ bt_name = "#{path_part}="
286
+ unless sub_next || (klass == sub_obj.class && criteria.empty?)
287
+ sub_next = klass.new(criteria || {})
288
+ to_be_saved << [sub_next, sub_obj, bt_name, sub_next]
289
+ end
290
+ # This wires it up in memory, but doesn't yet put the proper foreign key ID in
291
+ # place when the primary object is a new one (and thus not having yet been saved
292
+ # doesn't yet have an ID).
293
+ # binding.pry if !sub_next.new_record? && sub_next.name == 'Squidget Widget' # !sub_obj.changed?
294
+ # # %%% Question is -- does the above set changed? when a foreign key is not yet set
295
+ # # and only the in-memory object has changed?
296
+ is_yet_changed = sub_obj.changed?
297
+ sub_obj.send(bt_name, sub_next)
298
+
299
+ # We need this in the case of updating the primary object across a belongs_to when
300
+ # the foreign one already exists and is not otherwise changing, such as when it is
301
+ # not a new one, so wouldn't otherwise be getting saved.
302
+ to_be_saved << [sub_obj] if !sub_obj.new_record? && !is_yet_changed && sub_obj.changed?
303
+ # From a has_many or has_one?
304
+ # Rails 4.0 and later can do: sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
305
+ elsif [:has_many, :has_one].include?(assoc.macro) && !assoc.options[:through]
306
+ ::DutyFree::Extensions._save_pending(to_be_saved)
318
307
  # Try to find a unique item if one is referenced
319
308
  # %%% There is possibility that when bringing in related classes using a nil
320
309
  # in IMPORT_TEMPLATE[:all] that this will break. Need to test deeply nested things.
321
310
  start = (v.pre_prefix.blank? ? 0 : v.pre_prefix.length)
322
311
  trim_prefix = v.titleize[start..-(v.name.length + 2)]
323
312
  trim_prefix << ' ' unless trim_prefix.blank?
324
- klass = sub_next.klass
325
- # binding.pry if klass.name == 'OrderDetail'
326
-
327
313
  # assoc.inverse_of is the belongs_to side of the has_many train we came in here on.
328
- sub_hm, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
329
-
314
+ sub_hm, criteria = assoc.klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
330
315
  # If still not found then create a new related object using this has_many collection
331
- sub_next = sub_hm || sub_next.new(criteria)
316
+ # (criteria.empty? ? nil : sub_next.new(criteria))
317
+ if sub_hm
318
+ sub_next = sub_hm
319
+ elsif assoc.macro == :has_one
320
+ # assoc.active_record.name.underscore is only there to support older Rails
321
+ # that doesn't do automatic inverse_of
322
+ ho_name = "#{assoc.inverse_of&.name || assoc.active_record.name.underscore}="
323
+ sub_next = assoc.klass.new(criteria)
324
+ to_be_saved << [sub_next, sub_next, ho_name, sub_obj]
325
+ else
326
+ # Two other methods that are possible to check for here are :conditions and
327
+ # :sanitized_conditions, which do not exist in Rails 4.0 and later.
328
+ sub_next = if assoc.respond_to?(:require_association)
329
+ # With Rails < 4.0, sub_next could come in as an Array or a broken CollectionProxy
330
+ assoc.klass.new({ fk_from(assoc) => sub_obj.send(sub_obj.class.primary_key) }.merge(criteria))
331
+ else
332
+ sub_next.proxy_association.reflection.instance_variable_set(:@foreign_key, fk_from(assoc))
333
+ sub_next.new(criteria)
334
+ end
335
+ to_be_saved << [sub_next]
336
+ end
337
+ end
338
+ # Look for possible missing polymorphic detail
339
+ # Maybe can test for this via assoc.through_reflection
340
+ if assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) &&
341
+ (delegate = assoc.send(:delegate_reflection)&.active_record&.reflect_on_association(assoc.source_reflection_name)) &&
342
+ delegate.options[:polymorphic]
343
+ polymorphics << { parent: sub_next, child: sub_obj, type_col: delegate.foreign_type, id_col: delegate.foreign_key.to_s }
332
344
  end
333
345
  unless sub_next.nil?
334
346
  # if sub_next.class.name == devise_class && # only for Devise users
@@ -342,14 +354,13 @@ module DutyFree
342
354
  sub_objects[this_path] = sub_next if this_path.present?
343
355
  end
344
356
  end
345
- sub_obj = sub_next unless sub_next.nil?
357
+ sub_obj = sub_next
346
358
  end
347
359
  next if sub_obj.nil?
348
360
 
349
- sym = "#{v.name}=".to_sym
350
- next unless sub_obj.respond_to?(sym)
361
+ next unless sub_obj.respond_to?(sym = "#{v.name}=".to_sym)
351
362
 
352
- col_type = (sub_class = sub_obj.class).columns_hash[v.name.to_s]&.type
363
+ col_type = sub_obj.class.columns_hash[v.name.to_s]&.type
353
364
  if col_type.nil? && (virtual_columns = import_template[:virtual_columns]) &&
354
365
  (virtual_columns = virtual_columns[this_path] || virtual_columns)
355
366
  col_type = virtual_columns[v.name]
@@ -357,28 +368,27 @@ module DutyFree
357
368
  if col_type == :boolean
358
369
  if row[key].nil?
359
370
  # Do nothing when it's nil
360
- elsif %w[yes y].include?(row[key]&.downcase) # Used to cover 'true', 't', 'on'
371
+ elsif %w[true t yes y].include?(row[key]&.strip&.downcase) # Used to cover 'on'
361
372
  row[key] = true
362
- elsif %w[no n].include?(row[key]&.downcase) # Used to cover 'false', 'f', 'off'
373
+ elsif %w[false f no n].include?(row[key]&.strip&.downcase) # Used to cover 'off'
363
374
  row[key] = false
364
375
  else
365
376
  row_errors[v.name] ||= []
366
377
  row_errors[v.name] << "Boolean value \"#{row[key]}\" in column #{key + 1} not recognized"
367
378
  end
368
379
  end
380
+
381
+ is_yet_changed = sub_obj.changed?
369
382
  sub_obj.send(sym, row[key])
383
+ # If this one is transitioning from having not changed to now having been changed,
384
+ # and is not a new one anyway that would already be lined up to get saved, then we
385
+ # mark it to now be saved.
386
+ to_be_saved << [sub_obj] if !sub_obj.new_record? && !is_yet_changed && sub_obj.changed?
370
387
  # else
371
- # puts " #{sub_class.name} doesn't respond to #{sym}"
372
- end
373
- # Try to save a final sub-object if one exists
374
- sub_obj.save if sub_obj && this_path && !is_has_one && sub_obj.valid?
375
-
376
- # Wire up has_one associations
377
- has_ones.each do |hasone|
378
- parent = sub_objects[hasone[0..-2].map(&:to_s).join(',')] || obj
379
- hasone_object = sub_objects[hasone.map(&:to_s).join(',')]
380
- parent.send("#{hasone[-1]}=", hasone_object) if parent.new_record? || hasone_object.valid?
388
+ # puts " #{sub_obj.class.name} doesn't respond to #{sym}"
381
389
  end
390
+ # Try to save final sub-object(s) if any exist
391
+ ::DutyFree::Extensions._save_pending(to_be_saved)
382
392
 
383
393
  # Reinstate any missing polymorphic _type and _id values
384
394
  polymorphics.each do |poly|
@@ -415,26 +425,98 @@ module DutyFree
415
425
  s += v.last[1..-1].map { |line_num| { line_num => v.first } } if v.last.count > 1
416
426
  s
417
427
  end
418
- # Check to see if they want to do anything after the import
419
428
  ret = { inserted: inserts, updated: updates, duplicates: duplicates, errors: errors }
420
- if @after_import ||= (import_template[:after_import]) # || some generic after_import)
421
- ret = ret2 if (ret2 = @after_import.call(ret)).is_a?(Hash)
429
+
430
+ # Check to see if they want to do anything after the import
431
+ # First if defined in the import_template, then if there is a method in the class,
432
+ # and finally (not yet implemented) a generic global after_import
433
+ my_after_import = import_template[:after_import]
434
+ my_after_import ||= respond_to?(:after_import) && method(:after_import)
435
+ # my_after_import ||= some generic my_after_import
436
+ if my_after_import
437
+ last_arg_idx = my_after_import.parameters.length - 1
438
+ arguments = [ret][0..last_arg_idx]
439
+ ret = ret2 if (ret2 = my_after_import.call(*arguments)).is_a?(Hash)
422
440
  end
423
441
  end
424
442
  ret
425
443
  end
426
444
 
445
+ def fk_from(assoc)
446
+ # Try first to trust whatever they've marked as being the foreign_key, and then look
447
+ # at the inverse's foreign key setting if available. In all cases don't accept
448
+ # anything that's not backed with a real column in the table.
449
+ col_names = assoc.klass.column_names
450
+ if (fk_name = assoc.options[:foreign_key]) && col_names.include?(fk_name.to_s)
451
+ return fk_name
452
+ end
453
+ if (fk_name = assoc.inverse_of&.options&.fetch(:foreign_key) { nil }) &&
454
+ col_names.include?(fk_name.to_s)
455
+ return fk_name
456
+ end
457
+ if assoc.respond_to?(:foreign_key) && (fk_name = assoc.foreign_key) &&
458
+ col_names.include?(fk_name.to_s)
459
+ return fk_name
460
+ end
461
+ if (fk_name = assoc.inverse_of&.foreign_key) &&
462
+ col_names.include?(fk_name.to_s)
463
+ return fk_name
464
+ end
465
+ if (fk_name = assoc.inverse_of&.association_foreign_key) &&
466
+ col_names.include?(fk_name.to_s)
467
+ return fk_name
468
+ end
469
+ # Don't let this fool you -- we really are in search of the foreign key name here,
470
+ # and Rails 3.0 and older used some fairly interesting conventions, calling it instead
471
+ # the "primary_key_name"!
472
+ if assoc.respond_to?(:primary_key_name)
473
+ if (fk_name = assoc.primary_key_name) && col_names.include?(fk_name.to_s)
474
+ return fk_name
475
+ end
476
+ if (fk_name = assoc.inverse_of.primary_key_name) && col_names.include?(fk_name.to_s)
477
+ return fk_name
478
+ end
479
+ end
480
+
481
+ puts "* Wow, had no luck at all finding a foreign key for #{assoc.inspect}"
482
+ end
483
+
427
484
  # For use with importing, based on the provided column list calculate all valid combinations
428
485
  # of unique columns. If there is no valid combination, throws an error.
429
486
  # Returns an object found by this means.
430
- def find_existing(uniques, cols, starred, import_template, keepers, train_we_came_in_here_on, row = nil, obj = nil, all = nil, trim_prefix = '')
431
- col_name_offset = trim_prefix.length
432
- @valid_uniques ||= {} # Fancy memoisation
487
+ def find_existing(uniques, cols, starred, import_template, keepers, train_we_came_in_here_on,
488
+ row = nil, klass_or_collection = nil, template_all = nil, trim_prefix = '')
489
+ unless trim_prefix.blank?
490
+ cols = cols.map { |c| c.start_with?(trim_prefix) ? c[trim_prefix.length..-1] : nil }
491
+ starred = starred.each_with_object([]) do |v, s|
492
+ s << v[trim_prefix.length..-1] if v.start_with?(trim_prefix)
493
+ s
494
+ end
495
+ end
433
496
  col_list = cols.join('|')
434
- unless (vus = @valid_uniques[col_list])
435
- # Find all unique combinations that are available based on incoming columns, and
436
- # pair them up with column number mappings.
437
- template_column_objects = ::DutyFree::Extensions._recurse_def(self, all || import_template[:all], import_template).first
497
+
498
+ # First add in foreign key stuff we can find from belongs_to associations (other than the
499
+ # one we might have arrived here upon).
500
+ criteria = {} # Enough detail to find or build a new object
501
+ bt_criteria = {}
502
+ bt_criteria_all_nil = true
503
+ bt_col_indexes = []
504
+ available_bts = []
505
+ only_valid_uniques = (train_we_came_in_here_on == false)
506
+ uniq_lookups = {} # The data, or how to look up the data
507
+
508
+ vus = ((@valid_uniques ||= {})[col_list] ||= {}) # Fancy memoisation
509
+
510
+ # First, get an overall list of AVAILABLE COLUMNS before considering tricky foreign key stuff.
511
+ # ============================================================================================
512
+ # Generate a list of column names matched up with their zero-ordinal column number mapping for
513
+ # all columns from the incoming import data.
514
+ if (is_new_vus = vus.empty?)
515
+ template_column_objects = ::DutyFree::Extensions._recurse_def(
516
+ self,
517
+ template_all || import_template[:all],
518
+ import_template
519
+ ).first
438
520
  available = if trim_prefix.blank?
439
521
  template_column_objects.select { |col| col.pre_prefix.blank? && col.prefix.blank? }
440
522
  else
@@ -444,113 +526,190 @@ module DutyFree
444
526
  trim_prefix_snake == "#{this_prefix}_"
445
527
  end
446
528
  end.map { |avail| avail.name.to_s.titleize }
447
- vus = defined_uniques(uniques, cols, nil, starred).select do |k, _v|
448
- is_good = true
449
- k.each do |k_col|
450
- unless k_col.start_with?(trim_prefix) && available.include?(k_col[col_name_offset..-1])
451
- is_good = false
452
- break
529
+ end
530
+
531
+ # Process FOREIGN KEY stuff by going through each belongs_to in this model.
532
+ # =========================================================================
533
+ # This list of all valid uniques will help to filter which foreign keys are kept, and also
534
+ # get further filtered later to arrive upon a final set of valid uniques. (Often but not
535
+ # necessarily a specific valid unique as perhaps with a list of users you want to update some
536
+ # folks based on having their email as a unique identifier, and other folks by having a
537
+ # combination of their name and street address as unique, and use both of those possible
538
+ # unique variations to update phone numbers, and do that all as a part of one import.)
539
+ all_vus = defined_uniques(uniques, cols, col_list, starred, trim_prefix)
540
+
541
+ # %%% Ultimately may consider making this recursive
542
+ reflect_on_all_associations.each do |sn_bt|
543
+ next unless sn_bt.belongs_to? && (!train_we_came_in_here_on || sn_bt != train_we_came_in_here_on)
544
+
545
+ # # %%% Make sure there's a starred column we know about from this one
546
+ # uniq_lookups[sn_bt.foreign_key] = nil if only_valid_uniques
547
+
548
+ # This search prefix becomes something like "Order Details Product "
549
+ cols.each_with_index do |bt_col, idx|
550
+ next if bt_col_indexes.include?(idx) ||
551
+ !bt_col&.start_with?(bt_prefix = (trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} "))
552
+
553
+ available_bts << bt_col
554
+ fk_id = if row
555
+ # Max ID so if there are multiple matches, only the most recent one is picked.
556
+ # %%% Need to stack these up in case there are multiple
557
+ # (like first_name, last_name on a referenced employee)
558
+ sn_bt.klass.where(keepers[idx].name => row[idx]).limit(1).pluck(MAX_ID).first
559
+ else
560
+ # elsif is_new_vus
561
+ # # Add to our criteria if this belongs_to is required
562
+ # bt_req_by_default = sn_bt.klass.respond_to?(:belongs_to_required_by_default) &&
563
+ # sn_bt.klass.belongs_to_required_by_default
564
+ # unless !vus.values.first&.include?(idx) &&
565
+ # (sn_bt.options[:optional] || (sn_bt.options[:required] == false) || !bt_req_by_default)
566
+ # # # Add this fk to the criteria
567
+ # # criteria[fk_name] = fk_id
568
+
569
+ # ref = [keepers[idx].name, idx]
570
+ # # bt_criteria[(fk_name = sn_bt.foreign_key)] ||= [sn_bt.klass, []]
571
+ # # bt_criteria[fk_name].last << ref
572
+ # # bt_criteria[bt_col] = [sn_bt.klass, ref]
573
+
574
+ # # Maybe this is the most useful
575
+ # # First array is friendly column names, second is references
576
+ # foreign_uniques = (bt_criteria[sn_bt.name] ||= [sn_bt.klass, [], []])
577
+ # foreign_uniques[1] << ref
578
+ # foreign_uniques[2] << bt_col
579
+ # vus[bt_col] = foreign_uniques # And we can look up this growing set from any foreign column
580
+ [sn_bt.klass, keepers[idx].name, idx]
581
+ end
582
+ if fk_id
583
+ bt_col_indexes << idx
584
+ bt_criteria_all_nil = false
585
+ end
586
+ # If we're processing a row then this list of foreign key column name entries, named such as
587
+ # "order_id" or "product_id" instead of column-specific stuff like "Order Date" and "Product Name",
588
+ # is kept until the last and then gets merged on top of the other criteria before being returned.
589
+ bt_criteria[(fk_name = sn_bt.foreign_key)] = fk_id
590
+
591
+ # Check to see if belongs_tos are generally required on this specific table
592
+ bt_req_by_default = sn_bt.klass.respond_to?(:belongs_to_required_by_default) &&
593
+ sn_bt.klass.belongs_to_required_by_default
594
+
595
+ # Add to our CRITERIA just the belongs_to things that check out.
596
+ # ==============================================================
597
+ # The first check, "all_vus.keys.first.none? { |k| k.start_with?(bt_prefix) }"
598
+ # is to see if one of the columns we're working with from the unique that we've chosen
599
+ # comes from the table referenced by this belongs_to (sn_bt).
600
+ #
601
+ # The second check on the :optional option and bt_req_by_default comes directly from
602
+ # how Rails 5 and later checks to see if a specific belongs_to is marked optional
603
+ # (or required), and without having that indication will fall back on checking the model
604
+ # itself to see if it requires belongs_tos by default.
605
+ next if all_vus.keys.first.none? { |k| k.start_with?(bt_prefix) } &&
606
+ (sn_bt.options[:optional] || !bt_req_by_default)
607
+
608
+ # Add to the criteria
609
+ criteria[fk_name] = fk_id
610
+ end
611
+ end
612
+
613
+ # Now circle back find a final list of VALID UNIQUES by re-assessing the list of all valid uniques
614
+ # in relation to the available belongs_tos found in the last foreign key step.
615
+ if is_new_vus
616
+ available += available_bts
617
+ all_vus.each do |k, v|
618
+ combined_k = []
619
+ combined_v = []
620
+ k.each_with_index do |key, idx|
621
+ if available.include?(key)
622
+ combined_k << key
623
+ combined_v << v[idx]
453
624
  end
454
625
  end
455
- is_good
626
+ vus[combined_k] = combined_v unless combined_k.empty?
456
627
  end
457
- @valid_uniques[col_list] = vus
458
628
  end
459
629
 
630
+ # uniq_lookups = vus.inject({}) do |s, v|
631
+ # return s if available_bts.include?(v.first) # These will be provided in criteria, and not uniq_lookups
632
+
633
+ # # uniq_lookups[k[trim_prefix.length..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
634
+ # s[v.first.downcase.tr(' ', '_').to_sym] = v.last
635
+ # s
636
+ # end
637
+
638
+ new_criteria_all_nil = bt_criteria_all_nil
639
+
460
640
  # Make sure they have at least one unique combination to take cues from
461
- ret = {}
462
641
  unless vus.empty? # raise NoUniqueColumnError.new(I18n.t('import.no_unique_column_error'))
463
642
  # Convert the first entry to a simplified hash, such as:
464
643
  # {[:investigator_institutions_name, :investigator_institutions_email] => [8, 9], ...}
465
644
  # to {:name => 8, :email => 9}
466
- key, val = vus.first
645
+ key, val = vus.first # Utilise the first identified set of valid uniques
467
646
  key.each_with_index do |k, idx|
468
- ret[k[col_name_offset..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
469
- end
470
- end
647
+ next if available_bts.include?(k) # These will be provided in criteria, and not uniq_lookups
471
648
 
472
- # %%% If uniqueness is based on something else hanging out on a belongs_to then we're pretty hosed.
473
- # (Case in point, importing Order with related Order Detail and Product, and then Product needs to
474
- # be found or built first before OrderDetail.)
475
- # Might have to do a deferred save kind of thing, and also make sure the product stuff came first
476
- # before the other stuff
477
-
478
- # Add in any foreign key stuff we can find from other belongs_to associations
479
- # %%% This is starting to look like the other BelongsToAssociation code above around line
480
- # 697, so it really needs to be turned into something recursive instead of this two-layer
481
- # thick thing at best.
482
- bts = reflect_on_all_associations.each_with_object([]) do |sn_assoc, s|
483
- if sn_assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection) &&
484
- (!train_we_came_in_here_on || sn_assoc != train_we_came_in_here_on) &&
485
- sn_assoc.klass != self # Omit stuff pointing to us (like self-referencing stuff)
486
- # %%% Make sure there's a starred column we know about from this one
487
- ret[sn_assoc.foreign_key] = nil if train_we_came_in_here_on == false
488
- s << sn_assoc
489
- end
490
- s
491
- end
492
-
493
- # Find by all corresponding columns
494
- criteria = {}
495
- if train_we_came_in_here_on != false
496
- criteria = ret.each_with_object({}) do |v, s|
497
- s[v.first.to_sym] = row[v.last]
498
- s
499
- end
500
- end
649
+ # uniq_lookups[k[trim_prefix.length..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
650
+ k_sym = k.downcase.tr(' ', '_').to_sym
651
+ v = val[idx]
652
+ uniq_lookups[k_sym] = v # The column number in which to find the data
501
653
 
502
- bts.each do |sn_bt|
503
- # This search prefix becomes something like "Order Details Product "
504
- cols.each_with_index do |bt_col, idx|
505
- next unless bt_col.start_with?(trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} ")
654
+ next if only_valid_uniques || bt_col_indexes.include?(v)
506
655
 
507
- fk_id = if row
508
- # Max ID so if there are multiple, only the most recent one is picked.
509
- # %%% Need to stack these up in case there are multiple (like first_name, last_name on a referenced employee)
510
- sn_bt.klass.where(keepers[idx].name => row[idx]).limit(1).pluck('MAX(id)').first
511
- else
512
- [sn_bt.klass, keepers[idx].name, idx]
513
- end
514
- criteria[sn_bt.foreign_key] = fk_id
656
+ # Find by all corresponding columns
657
+ if (row_value = row[v])
658
+ new_criteria_all_nil = false
659
+ criteria[k_sym] = row_value # The data, or how to look up the data
660
+ end
515
661
  end
516
662
  end
517
663
 
518
- # Short-circuiting this to only get back the valid_uniques?
519
- return ret.merge(criteria) if train_we_came_in_here_on == false
520
-
521
- # With this criteria, find any matching has_many row we can so we can update it
522
- sub_hm = obj.find do |hm_obj|
523
- is_good = true
524
- criteria.each do |k, v|
525
- if hm_obj.send(k).to_s != v.to_s
526
- is_good = false
527
- break
664
+ return uniq_lookups.merge(criteria) if only_valid_uniques
665
+
666
+ # If there's nothing to match upon then we're out
667
+ return [nil, {}] if new_criteria_all_nil
668
+
669
+ # With this criteria, find any matching has_many row we can so we can update it.
670
+ # First try directly looking it up through ActiveRecord.
671
+ found_object = klass_or_collection.find_by(criteria)
672
+ # If not successful, such as when fields are exposed via helper methods instead of being
673
+ # real columns in the database tables, try this more intensive approach. This is useful
674
+ # if you had full name kind of data coming in on a spreadsheeet, but in the destination
675
+ # table it's broken out to first_name, middle_name, surname. By writing both full_name
676
+ # and full_name= methods, the importer can check to see if this entry is already there,
677
+ # and put a new row in if not, having one incoming name break out to populate three
678
+ # destination columns.
679
+ unless found_object || klass_or_collection.is_a?(Array)
680
+ found_object = klass_or_collection.find do |obj|
681
+ is_good = true
682
+ criteria.each do |k, v|
683
+ if obj.send(k).to_s != v.to_s
684
+ is_good = false
685
+ break
686
+ end
528
687
  end
688
+ is_good
529
689
  end
530
- is_good
531
690
  end
532
- # Try looking it up through ActiveRecord
533
- # %%% Should we perhaps do this first before the more intensive find routine above?
534
- sub_hm = obj.find_by(criteria) if sub_hm.nil?
535
- [sub_hm, criteria]
691
+ # Standard criteria as well as foreign key column name detail with exact foreign keys
692
+ # that match up to a primary key so that if needed a new related object can be built,
693
+ # complete with all its association detail.
694
+ [found_object, criteria.merge(bt_criteria)]
536
695
  end
537
696
 
538
697
  private
539
698
 
540
- def defined_uniques(uniques, cols = [], col_list = nil, starred = [])
699
+ def defined_uniques(uniques, cols = [], col_list = nil, starred = [], trim_prefix = '')
541
700
  col_list ||= cols.join('|')
542
- @defined_uniques ||= {}
543
- unless (defined_uniq = @defined_uniques[col_list])
701
+ unless (defined_uniq = (@defined_uniques ||= {})[col_list])
544
702
  utilised = {} # Track columns that have been referenced thusfar
545
703
  defined_uniq = uniques.each_with_object({}) do |unique, s|
546
704
  if unique.is_a?(Array)
547
705
  key = []
548
706
  value = []
549
707
  unique.each do |unique_part|
550
- val = cols.index(unique_part_name = unique_part.to_s.titleize)
551
- next if val.nil?
708
+ val = (unique_part_name = unique_part.to_s.titleize).start_with?(trim_prefix) &&
709
+ cols.index(upn = unique_part_name[trim_prefix.length..-1])
710
+ next unless val
552
711
 
553
- key << unique_part_name
712
+ key << upn
554
713
  value << val
555
714
  end
556
715
  unless key.empty?
@@ -558,25 +717,62 @@ module DutyFree
558
717
  utilised[key] = nil
559
718
  end
560
719
  else
561
- val = cols.index(unique_part_name = unique.to_s.titleize)
562
- unless val.nil?
563
- s[[unique_part_name]] = [val]
564
- utilised[[unique_part_name]] = nil
720
+ val = (unique_name = unique.to_s.titleize).start_with?(trim_prefix) &&
721
+ cols.index(un = unique_name[trim_prefix.length..-1])
722
+ if val
723
+ s[[un]] = [val]
724
+ utilised[[un]] = nil
565
725
  end
566
726
  end
727
+ s
728
+ end
729
+ if defined_uniq.empty?
730
+ (starred - utilised.keys).each { |star| defined_uniq[[star]] = [cols.index(star)] }
731
+ # %%% puts "Tried to establish #{defined_uniq.inspect}"
567
732
  end
568
- (starred - utilised.keys).each { |star| defined_uniq[[star]] = [cols.index(star)] }
569
733
  @defined_uniques[col_list] = defined_uniq
570
734
  end
571
735
  defined_uniq
572
736
  end
573
737
  end # module ClassMethods
574
738
 
739
+ # Called before building any object linked through a has_one or has_many so that foreign key IDs
740
+ # can be added properly to those new objects. Finally at the end also called to save everything.
741
+ def self._save_pending(to_be_saved)
742
+ while (tbs = to_be_saved.pop)
743
+ # puts "Will be calling #{tbs[1].class.name} #{tbs[1]&.id} .#{tbs[2]} #{tbs[3].class.name} #{tbs[3]&.id}"
744
+
745
+ # Wire this one up if it had came from a has_one association
746
+ tbs[1].send(tbs[2], tbs[3]) if tbs[0] == tbs[1]
747
+
748
+ ais = (tbs.first.class.respond_to?(:around_import_save) && tbs.first.class.method(:around_import_save)) ||
749
+ (respond_to?(:around_import_save) && method(:around_import_save))
750
+ if ais
751
+ # Send them the sub_obj even if it might be invalid so they can choose
752
+ # to make it valid if they wish.
753
+ ais.call(tbs.first) do |modded_obj = nil|
754
+ modded_obj = (modded_obj || tbs.first)
755
+ modded_obj.save if modded_obj&.valid?
756
+ end
757
+ elsif tbs.first.valid?
758
+ tbs.first.save
759
+ else
760
+ puts "* Unable to save #{tbs.first.inspect}"
761
+ end
762
+
763
+ next if tbs[1].nil? || # From a has_many?
764
+ tbs[0] == tbs[1] || # From a has_one?
765
+ tbs.first.new_record?
766
+
767
+ tbs[1].send(tbs[2], tbs[3])
768
+ end
769
+ end
770
+
575
771
  # The snake-cased column alias names used in the query to export data
576
772
  def self._template_columns(klass, import_template = nil)
577
773
  template_detail_columns = klass.instance_variable_get(:@template_detail_columns)
578
- if (template_import_columns = klass.instance_variable_get(:@template_import_columns)) != import_template
579
- klass.instance_variable_set(:@template_import_columns, template_import_columns = import_template)
774
+ if klass.instance_variable_get(:@template_import_columns) != import_template
775
+ klass.instance_variable_set(:@template_import_columns, import_template)
580
776
  klass.instance_variable_set(:@template_detail_columns, (template_detail_columns = nil))
581
777
  end
582
778
  unless template_detail_columns
@@ -589,16 +785,21 @@ module DutyFree
589
785
 
590
786
  # Recurse and return two arrays -- one with all columns in sequence, and one a hierarchy of
591
787
  # nested hashes to be used with ActiveRecord's .joins() to facilitate export.
592
- def self._recurse_def(klass, array, import_template, assocs = [], joins = [], pre_prefix = '', prefix = '')
788
+ def self._recurse_def(klass, array, import_template, order_by = [], assocs = [], joins = [], pre_prefix = '', prefix = '')
789
+ prefixes = ::DutyFree::Util._prefix_join([pre_prefix, prefix])
593
790
  # Confirm we can actually navigate through this association
594
791
  prefix_assoc = (assocs.last&.klass || klass).reflect_on_association(prefix) if prefix.present?
595
- assocs = assocs.dup << prefix_assoc unless prefix_assoc.nil?
596
- prefixes = ::DutyFree::Util._prefix_join([pre_prefix, prefix])
792
+ if prefix_assoc
793
+ assocs = assocs.dup << prefix_assoc
794
+ if prefix_assoc.macro == :has_many && (pk = prefix_assoc.active_record.primary_key)
795
+ order_by << ["#{prefixes.tr('.', '_')}_", pk]
796
+ end
797
+ end
597
798
  array = array.inject([]) do |s, col|
598
799
  s + if col.is_a?(Hash)
599
800
  col.inject([]) do |s2, v|
600
801
  joins << { v.first.to_sym => (joins_array = []) }
601
- s2 += _recurse_def(klass, (v.last.is_a?(Array) ? v.last : [v.last]), import_template, assocs, joins_array, prefixes, v.first.to_sym).first
802
+ 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
602
803
  end
603
804
  elsif col.nil?
604
805
  if assocs.empty?
@@ -607,7 +808,7 @@ module DutyFree
607
808
  # Bring in from another class
608
809
  joins << { prefix => (joins_array = []) }
609
810
  # %%% Also bring in uniques and requireds
610
- _recurse_def(klass, assocs.last.klass::IMPORT_TEMPLATE[:all], import_template, assocs, joins_array, prefixes).first
811
+ _recurse_def(klass, assocs.last.klass::IMPORT_TEMPLATE[:all], import_template, order_by, assocs, joins_array, prefixes).first
611
812
  end
612
813
  else
613
814
  [::DutyFree::Column.new(col, pre_prefix, prefix, assocs, klass, import_template[:as])]
@@ -617,9 +818,13 @@ module DutyFree
617
818
  end
618
819
  end # module Extensions
619
820
 
620
- class NoUniqueColumnError < ActiveRecord::RecordNotUnique
821
+ # Rails < 4.0 doesn't have ActiveRecord::RecordNotUnique, so use the more generic ActiveRecord::ActiveRecordError instead
822
+ ar_not_unique_error = ActiveRecord.const_defined?('RecordNotUnique') ? ActiveRecord::RecordNotUnique : ActiveRecord::ActiveRecordError
823
+ class NoUniqueColumnError < ar_not_unique_error
621
824
  end
622
825
 
623
- class LessThanHalfAreMatchingColumnsError < ActiveRecord::RecordInvalid
826
+ # Rails < 4.2 doesn't have ActiveRecord::RecordInvalid, so use the more generic ActiveRecord::ActiveRecordError instead
827
+ ar_invalid_error = ActiveRecord.const_defined?('RecordInvalid') ? ActiveRecord::RecordInvalid : ActiveRecord::ActiveRecordError
828
+ class LessThanHalfAreMatchingColumnsError < ar_invalid_error
624
829
  end
625
830
  end