duty_free 1.0.2 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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