duty_free 1.0.7 → 1.0.8

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: b16ccd7258656d274fc6b1f8798c5b5d1f3822570bdd9600f10d55e486a1308d
4
- data.tar.gz: 9174b6bb8521edfacd9ddde72decc8a5b9274f33049740914cdd5077f9d52c59
3
+ metadata.gz: 714b772aa09eb15409cd7c58d4ccb64f967caf41f2a13e7813e90cc94d7eb48f
4
+ data.tar.gz: 750dfe36b729979a0356f14f8588568822ae22911c142d294358231504daa78e
5
5
  SHA512:
6
- metadata.gz: 8ba849219eca3426c396be95fc29828c9fe60cd4c538004796d9cc9c17d8be11541c1b18f3d6ed0a89e2980570caafe88dbfc9772ca2d48fe17239c5417d71d8
7
- data.tar.gz: 54fa0c8b8bd4719863ebeb246f1d1fd21decec7ed1061b9ab8b447e2d38216a52ad427e2a3964564d19a6016ee572b6a95d7f269ed409e90df4c96cbbec01910
6
+ metadata.gz: 9d77a228b4e4990dad39882a4e67d1009f3c16114bc0e9b24280e1f8e9437c7fc55a8884eb37e8f6c15898958833c7f79bb7984d721f4dbd1fce7311ca6421d0
7
+ data.tar.gz: 1d35f3197ab500a6210eecc561190c02a04d87e3d4b2634a9a62695488d4e2ed0825f7240836aa9d32fe8ce01e06be38ddccda1955ca3d3669fd7ad18e8335f2
@@ -21,8 +21,8 @@ if ActiveRecord.version < ::Gem::Version.new('5.0') &&
21
21
  end
22
22
  end
23
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:
24
+ # Allow ActiveRecord 4.0 and 4.1 to work with newer Ruby (>= 2.4) by avoiding a "stack level too deep"
25
+ # error when ActiveSupport tries to smarten up Numeric by messing with Fixnum and Bignum at the end of:
26
26
  # activesupport-4.0.13/lib/active_support/core_ext/numeric/conversions.rb
27
27
  if ActiveRecord.version < ::Gem::Version.new('4.2') &&
28
28
  ActiveRecord.version > ::Gem::Version.new('3.2') &&
@@ -33,7 +33,7 @@ if ActiveRecord.version < ::Gem::Version.new('4.2') &&
33
33
  Numeric.const_set('Bignum', OurBignum)
34
34
  end
35
35
 
36
- # Allow Rails < 3.2 to run with newer versions of Psych gem
36
+ # Allow ActiveRecord < 3.2 to run with newer versions of Psych gem
37
37
  if BigDecimal.respond_to?(:yaml_tag) && !BigDecimal.respond_to?(:yaml_as)
38
38
  class BigDecimal
39
39
  class <<self
@@ -42,6 +42,28 @@ if BigDecimal.respond_to?(:yaml_tag) && !BigDecimal.respond_to?(:yaml_as)
42
42
  end
43
43
  end
44
44
 
45
+ require 'duty_free/util'
46
+
47
+ # Allow ActiveRecord < 3.2 to work with Ruby 2.7 and later
48
+ if ActiveRecord.version < ::Gem::Version.new('3.2') &&
49
+ ::Gem::Version.new(RUBY_VERSION) >= ::Gem::Version.new('2.7')
50
+ # Remove circular reference for "now"
51
+ ::DutyFree::Util._patch_require(
52
+ 'active_support/values/time_zone.rb', '/activesupport',
53
+ ' def parse(str, now=now)',
54
+ ' def parse(str, now=now())'
55
+ )
56
+ # Remove circular reference for "reflection" for ActiveRecord 3.1
57
+ if ActiveRecord.version >= ::Gem::Version.new('3.1')
58
+ ::DutyFree::Util._patch_require(
59
+ 'active_record/associations/has_many_association.rb', '/activerecord',
60
+ 'reflection = reflection)',
61
+ 'reflection = reflection())',
62
+ :HasManyAssociation # Make sure the path for this guy is available to be autoloaded
63
+ )
64
+ end
65
+ end
66
+
45
67
  require 'active_record'
46
68
 
47
69
  require 'duty_free/config'
@@ -107,14 +129,19 @@ end
107
129
  # Major compatibility fixes for ActiveRecord < 4.2
108
130
  # ================================================
109
131
  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:
132
+ # Rails < 4.0 cannot do #find_by, #find_or_create_by, or do #pluck on multiple columns, so here are the patches:
111
133
  if ActiveRecord.version < ::Gem::Version.new('4.0')
112
134
  module ActiveRecord
113
- module Calculations # Normally find_by is in FinderMethods, which older AR doesn't have
135
+ # Normally find_by is in FinderMethods, which older AR doesn't have
136
+ module Calculations
114
137
  def find_by(*args)
115
138
  where(*args).limit(1).to_a.first
116
139
  end
117
140
 
141
+ def find_or_create_by(attributes, &block)
142
+ find_by(attributes) || create(attributes, &block)
143
+ end
144
+
118
145
  def pluck(*column_names)
119
146
  column_names.map! do |column_name|
120
147
  if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s)
@@ -174,7 +201,7 @@ ActiveSupport.on_load(:active_record) do
174
201
  unless Base.is_a?(Calculations)
175
202
  class Base
176
203
  class << self
177
- delegate :pluck, :find_by, to: :scoped
204
+ delegate :pluck, :find_by, :find_or_create_by, to: :scoped
178
205
  end
179
206
  end
180
207
  end
@@ -205,50 +232,105 @@ ActiveSupport.on_load(:active_record) do
205
232
  end
206
233
  end
207
234
  end
208
- end
209
- end
210
235
 
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
236
+ # ActiveRecord 3.1 and 3.2 didn't try to bring in &block for the .extending() convenience thing
237
+ # that smartens up scopes, and Ruby 2.7 complained loudly about just doing the magical "Proc.new"
238
+ # that historically would just capture the incoming block.
239
+ module QueryMethods
240
+ unless instance_method(:extending).parameters.include?([:block, :block])
241
+ # These first two lines used to be:
242
+ # def extending(*modules)
243
+ # modules << Module.new(&Proc.new) if block_given?
244
+
245
+ def extending(*modules, &block)
246
+ modules << Module.new(&block) if block_given?
247
+
248
+ return self if modules.empty?
220
249
 
221
- class Dot < Visitor
222
- alias visit_Integer visit_String
250
+ relation = clone
251
+ relation.send(:apply_modules, modules.flatten)
252
+ relation
223
253
  end
254
+ end
255
+ end
224
256
 
225
- class ToSql < Visitor
226
- private
257
+ # Same kind of thing for ActiveRecord::Scoping::Default#default_scope
258
+ module Scoping
259
+ module Default
260
+ module ClassMethods
261
+ if instance_methods.include?(:default_scope) &&
262
+ !instance_method(:default_scope).parameters.include?([:block, :block])
263
+ # Fix for AR 3.2-5.1
264
+ def default_scope(scope = nil, &block)
265
+ scope = block if block_given?
266
+
267
+ if scope.is_a?(Relation) || !scope.respond_to?(:call)
268
+ raise ArgumentError,
269
+ 'Support for calling #default_scope without a block is removed. For example instead ' \
270
+ "of `default_scope where(color: 'red')`, please use " \
271
+ "`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \
272
+ 'self.default_scope.)'
273
+ end
227
274
 
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
275
+ self.default_scopes += [scope]
232
276
  end
233
277
  end
234
- alias visit_Integer literal
235
278
  end
236
279
  end
237
280
  end
238
281
  end
239
282
  end
240
283
 
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
284
+ # Rails < 4.2 is not innately compatible with Ruby 2.4 and later, and comes up with:
285
+ # "TypeError: Cannot visit Integer" unless we patch like this:
286
+ if ::Gem::Version.new(RUBY_VERSION) >= ::Gem::Version.new('2.4') &&
287
+ Arel::Visitors.const_defined?('DepthFirst') &&
288
+ !Arel::Visitors::DepthFirst.private_instance_methods.include?(:visit_Integer)
289
+ module Arel
290
+ module Visitors
291
+ class DepthFirst < Visitor
292
+ alias visit_Integer terminal
293
+ end
294
+
295
+ class Dot < Visitor
296
+ alias visit_Integer visit_String
297
+ end
298
+
299
+ class ToSql < Visitor
300
+ private
301
+
302
+ # ActiveRecord before v3.2 uses Arel < 3.x, which does not have Arel#literal.
303
+ unless private_instance_methods.include?(:literal)
304
+ def literal(obj)
305
+ obj
306
+ end
307
+ end
308
+ alias visit_Integer literal
309
+ end
310
+ end
311
+ end
246
312
  end
247
313
 
248
314
  unless DateTime.instance_methods.include?(:nsec)
249
315
  class DateTime < Date
250
316
  def nsec
251
- (sec_fraction * 1000000000).to_i
317
+ (sec_fraction * 1_000_000_000).to_i
318
+ end
319
+ end
320
+ end
321
+
322
+ # First part of arel_table_type stuff:
323
+ # ------------------------------------
324
+ # (more found below)
325
+ if ActiveRecord.version < ::Gem::Version.new('5.0')
326
+ # Used by Util#_arel_table_type
327
+ module ActiveRecord
328
+ class Base
329
+ def self.arel_table
330
+ @arel_table ||= Arel::Table.new(table_name, arel_engine).tap do |x|
331
+ x.instance_variable_set(:@_arel_table_type, self)
332
+ end
333
+ end
252
334
  end
253
335
  end
254
336
  end
@@ -256,21 +338,89 @@ ActiveSupport.on_load(:active_record) do
256
338
  include ::DutyFree::Extensions
257
339
  end
258
340
 
259
- # # Require frameworks
260
- # if defined?(::Rails)
261
- # # Rails module is sometimes defined by gems like rails-html-sanitizer
262
- # # so we check for presence of Rails.application.
263
- # if defined?(::Rails.application)
264
- # require "duty_free/frameworks/rails"
265
- # else
266
- # ::Kernel.warn(<<-EOS.freeze
267
- # DutyFree has been loaded too early, before rails is loaded. This can
268
- # happen when another gem defines the ::Rails namespace, then DF is loaded,
269
- # all before rails is loaded. You may want to reorder your Gemfile, or defer
270
- # the loading of DF by using `require: false` and a manual require elsewhere.
271
- # EOS
272
- # )
273
- # end
274
- # else
275
- # require "duty_free/frameworks/active_record"
276
- # end
341
+ # Do this earlier because stuff here gets mixed into JoinDependency::JoinAssociation and AssociationScope
342
+ if ActiveRecord.version < ::Gem::Version.new('5.0') && Object.const_defined?('PG::Connection')
343
+ # Avoid pg gem deprecation warning: "You should use PG::Connection, PG::Result, and PG::Error instead"
344
+ PGconn = PG::Connection
345
+ PGresult = PG::Result
346
+ PGError = PG::Error
347
+ end
348
+
349
+ # More arel_table_type stuff:
350
+ # ---------------------------
351
+ if ActiveRecord.version < ::Gem::Version.new('5.2')
352
+ # Specifically for AR 3.1 and 3.2 to avoid: "undefined method `delegate' for ActiveRecord::Reflection::ThroughReflection:Class"
353
+ require 'active_support/core_ext/module/delegation' if ActiveRecord.version < ::Gem::Version.new('4.0')
354
+ # Used by Util#_arel_table_type
355
+ # rubocop:disable Style/CommentedKeyword
356
+ module ActiveRecord
357
+ module Reflection
358
+ # AR < 4.0 doesn't know about join_table and derive_join_table
359
+ unless AssociationReflection.instance_methods.include?(:join_table)
360
+ class AssociationReflection < MacroReflection
361
+ def join_table
362
+ @join_table ||= options[:join_table] || derive_join_table
363
+ end
364
+
365
+ private
366
+
367
+ def derive_join_table
368
+ [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", '_')
369
+ end
370
+ end
371
+ end
372
+ end
373
+
374
+ module Associations
375
+ # Specific to AR 4.2 - 5.1:
376
+ if Associations.const_defined?('JoinDependency') && JoinDependency.private_instance_methods.include?(:table_aliases_for)
377
+ class JoinDependency
378
+ private
379
+
380
+ if ActiveRecord.version < ::Gem::Version.new('5.1') # 4.2 or 5.0
381
+ def table_aliases_for(parent, node)
382
+ node.reflection.chain.map do |reflection|
383
+ alias_tracker.aliased_table_for(
384
+ reflection.table_name,
385
+ table_alias_for(reflection, parent, reflection != node.reflection)
386
+ ).tap do |x|
387
+ # %%% Specific only to Rails 4.2 (and maybe 4.1?)
388
+ x = x.left if x.is_a?(Arel::Nodes::TableAlias)
389
+ y = reflection.chain.find { |c| c.table_name == x.name }
390
+ x.instance_variable_set(:@_arel_table_type, y.klass)
391
+ end
392
+ end
393
+ end
394
+ end
395
+ end
396
+ elsif Associations.const_defined?('JoinHelper') && JoinHelper.private_instance_methods.include?(:construct_tables)
397
+ module JoinHelper
398
+ private
399
+
400
+ # AR > 3.0 and < 4.2 (%%% maybe only < 4.1?) uses construct_tables like this:
401
+ def construct_tables
402
+ tables = []
403
+ chain.each do |reflection|
404
+ tables << alias_tracker.aliased_table_for(
405
+ table_name_for(reflection),
406
+ table_alias_for(reflection, reflection != self.reflection)
407
+ ).tap do |x|
408
+ x = x.left if x.is_a?(Arel::Nodes::TableAlias)
409
+ x.instance_variable_set(:@_arel_table_type, reflection.chain.find { |c| c.table_name == x.name }.klass)
410
+ end
411
+
412
+ next unless reflection.source_macro == :has_and_belongs_to_many
413
+
414
+ tables << alias_tracker.aliased_table_for(
415
+ (reflection.source_reflection || reflection).join_table,
416
+ table_alias_for(reflection, true)
417
+ )
418
+ end
419
+ tables
420
+ end
421
+ end
422
+ end
423
+ end
424
+ end # module ActiveRecord
425
+ # rubocop:enable Style/CommentedKeyword
426
+ end
@@ -36,7 +36,7 @@ module DutyFree
36
36
  end
37
37
 
38
38
  def titleize
39
- @titleized ||= to_sym.titleize
39
+ @titleize ||= to_sym.titleize
40
40
  end
41
41
 
42
42
  def path
@@ -45,7 +45,7 @@ module DutyFree
45
45
 
46
46
  # The snake-cased column name to be used for building the full list of template_columns
47
47
  def to_sym
48
- @sym_string ||= ::DutyFree::Util._prefix_join(
48
+ @to_sym ||= ::DutyFree::Util._prefix_join(
49
49
  [pre_prefix, prefix, ::DutyFree::Util._clean_name(name, import_template_as)],
50
50
  '_'
51
51
  ).tr('.', '_')
@@ -6,7 +6,10 @@ require 'duty_free/suggest_template'
6
6
 
7
7
  # :nodoc:
8
8
  module DutyFree
9
+ # rubocop:disable Style/CommentedKeyword
9
10
  module Extensions
11
+ MAX_ID = Arel.sql('MAX(id)')
12
+
10
13
  def self.included(base)
11
14
  base.send :extend, ClassMethods
12
15
  base.send :extend, ::DutyFree::SuggestTemplate::ClassMethods
@@ -14,10 +17,6 @@ module DutyFree
14
17
 
15
18
  # :nodoc:
16
19
  module ClassMethods
17
- MAX_ID = Arel.sql('MAX(id)')
18
- # def self.extended(model)
19
- # end
20
-
21
20
  # Export at least column header, and optionally include all existing data as well
22
21
  def df_export(is_with_data = true, import_template = nil, use_inner_joins = false)
23
22
  use_inner_joins = true unless respond_to?(:left_joins)
@@ -53,8 +52,12 @@ module DutyFree
53
52
  if is_with_data
54
53
  order_by = []
55
54
  order_by << ['_', primary_key] if primary_key
55
+ all = import_template[:all] || import_template[:all!]
56
56
  # Automatically create a JOINs strategy and select list to get back all related rows
57
- template_cols, template_joins = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template, order_by)
57
+ template_cols, template_joins = ::DutyFree::Extensions._recurse_def(self, all, import_template, nil, order_by)
58
+ # We do this so early here because it removes type objects from template_joins so then
59
+ # template_joins can immediately be used for inner and outer JOINs.
60
+ our_names = [[self, '_']] + ::DutyFree::Util._recurse_arel(template_joins)
58
61
  relation = use_inner_joins ? joins(template_joins) : left_joins(template_joins)
59
62
 
60
63
  # So we can properly create the SELECT list, create a mapping between our
@@ -71,12 +74,27 @@ module DutyFree
71
74
  # With AR < 3.2, "froms" brings up the top node, an Arel::Nodes::InnerJoin
72
75
  ::DutyFree::Util._recurse_arel(core.froms)
73
76
  end
74
- our_names = ['_'] + ::DutyFree::Util._recurse_arel(template_joins)
75
- mapping = our_names.zip(arel_alias_names).to_h
77
+ # Make sure our_names lines up with the arel_alias_name by comparing the ActiveRecord type.
78
+ # AR < 5.0 behaves differently than newer versions, and AR 5.0 and 5.1 have a bug in the
79
+ # way the types get determined, so if we want perfect results then we must compensate for
80
+ # these foibles. Thank goodness that AR 5.2 and later find the proper type and make it
81
+ # available through the type_caster object, which is what we use when building the list of
82
+ # arel_alias_names.
83
+ mapping = arel_alias_names.each_with_object({}) do |arel_alias_name, s|
84
+ if our_names.first&.first == arel_alias_name.first
85
+ s[our_names.first.last] = arel_alias_name.last
86
+ our_names.shift
87
+ end
88
+ s
89
+ end
76
90
  relation = (order_by.empty? ? relation : relation.order(order_by.map { |o| "#{mapping[o.first]}.#{o.last}" }))
77
91
  # puts mapping.inspect
78
92
  # puts relation.dup.select(template_cols.map { |x| x.to_s(mapping) }).to_sql
79
- relation.select(template_cols.map { |x| x.to_s(mapping) }).each do |result|
93
+
94
+ # Allow customisation of query before running it
95
+ relation = yield(relation, mapping) if block_given?
96
+
97
+ relation&.select(template_cols.map { |x| x.to_s(mapping) })&.each do |result|
80
98
  rows << ::DutyFree::Extensions._template_columns(self, import_template).map do |col|
81
99
  value = result.send(col)
82
100
  case value
@@ -93,399 +111,57 @@ module DutyFree
93
111
  rows
94
112
  end
95
113
 
96
- # With an array of incoming data, the first row having column names, perform the import
97
114
  def df_import(data, import_template = nil)
98
- instance_variable_set(:@defined_uniques, nil)
99
- instance_variable_set(:@valid_uniques, nil)
100
-
101
- import_template ||= if constants.include?(:IMPORT_TEMPLATE)
102
- self::IMPORT_TEMPLATE
103
- else
104
- suggest_template(0, false, false)
105
- end
106
- # puts "Chose #{import_template}"
107
- inserts = []
108
- updates = []
109
- counts = Hash.new { |h, k| h[k] = [] }
110
- errors = []
111
-
112
- is_first = true
113
- uniques = nil
114
- cols = nil
115
- starred = []
116
- partials = []
117
- all = import_template[:all]
118
- keepers = {}
119
- valid_unique = nil
120
- existing = {}
121
- devise_class = ''
122
- ret = nil
123
-
124
- # Multi-tenancy gem Apartment can be used if there are separate schemas per tenant
125
- reference_models = if Object.const_defined?('Apartment')
126
- Apartment.excluded_models
127
- else
128
- []
129
- end
130
-
131
- if Object.const_defined?('Devise')
132
- Object.const_get('Devise') # If this fails, devise_class will remain a blank string.
133
- devise_class = Devise.mappings.values.first.class_name
134
- reference_models -= [devise_class]
135
- else
136
- devise_class = ''
137
- end
138
-
139
- # Did they give us a filename?
140
- if data.is_a?(String)
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')
144
- File.open(data)
145
- else
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
148
- CSV.new(data)
149
- end
150
- end
151
- # Or perhaps us a file?
152
- if data.is_a?(File)
153
- # Use the "roo" gem if it's available
154
- data = if Object.const_defined?('Roo::Spreadsheet', { csv_options: { encoding: 'bom|utf-8' } })
155
- Roo::Spreadsheet.open(data)
156
- else
157
- # Otherwise generic CSV parsing
158
- require 'csv' unless Object.const_defined?('CSV')
159
- CSV.open(data)
160
- end
161
- end
162
-
163
- # Will show as just one transaction when using auditing solutions such as PaperTrail
164
- ActiveRecord::Base.transaction do
165
- # Check to see if they want to do anything before the whole import
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)
175
- end
176
- data.each_with_index do |row, row_num|
177
- row_errors = {}
178
- if is_first # Anticipate that first row has column names
179
- uniques = import_template[:uniques]
180
-
181
- # Look for UTF-8 BOM in very first cell
182
- row[0] = row[0][3..-1] if row[0].start_with?([239, 187, 191].pack('U*'))
183
- # How about a first character of FEFF or FFFE to support UTF-16 BOMs?
184
- # FE FF big-endian (standard)
185
- # FF FE little-endian
186
- row[0] = row[0][1..-1] if [65_279, 65_534].include?(row[0][0].ord)
187
- cols = row.map { |col| (col || '').strip }
188
-
189
- # Unique column combinations can be called out explicitly in models using uniques: {...}, or to just
190
- # define one column at a time simply mark with an asterisk.
191
- # Track and clean up stars
192
- starred = cols.select do |col|
193
- if col[0] == '*'
194
- col.slice!(0)
195
- col.strip!
196
- end
197
- end
198
- partials = cols.select do |col|
199
- if col[0] == '~'
200
- col.slice!(0)
201
- col.strip!
202
- end
203
- end
204
- cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) }
205
- defined_uniques(uniques, cols, cols.join('|'), starred)
206
- # Make sure that at least half of them match what we know as being good column names
207
- template_column_objects = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template).first
208
- cols.each_with_index do |col, idx|
209
- # prefixes = col_detail.pre_prefix + (col_detail.prefix.blank? ? [] : [col_detail.prefix])
210
- keepers[idx] = template_column_objects.find { |col_obj| col_obj.titleize == col }
211
- # puts "Could not find a match for column #{idx + 1}, #{col}" if keepers[idx].nil?
212
- end
213
- raise ::DutyFree::LessThanHalfAreMatchingColumnsError, I18n.t('import.altered_import_template_coumns') if keepers.length < (cols.length / 2) - 1
214
-
215
- # Returns just the first valid unique lookup set if there are multiple
216
- valid_unique = find_existing(uniques, cols, starred, import_template, keepers, false)
217
- # Make a lookup from unique values to specific IDs
218
- existing = pluck(*([:id] + valid_unique.keys)).each_with_object(existing) do |v, s|
219
- s[v[1..-1].map(&:to_s)] = v.first
220
- s
221
- end
222
- is_first = false
223
- else # Normal row of data
224
- is_insert = false
225
- existing_unique = valid_unique.inject([]) do |s, v|
226
- s << if v.last.is_a?(Array)
227
- v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck(MAX_ID).first.to_s
228
- else
229
- row[v.last].to_s
230
- end
231
- end
232
- to_be_saved = []
233
- # Check to see if they want to preprocess anything
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]
240
- end
241
- sub_obj = nil
242
- is_has_one = false
243
- has_ones = []
244
- polymorphics = []
245
- sub_objects = {}
246
- this_path = nil
247
- keepers.each do |key, v|
248
- next if v.nil?
249
-
250
- sub_obj = obj
251
- this_path = +''
252
- # puts "p: #{v.path}"
253
- v.path.each_with_index do |path_part, idx|
254
- this_path << (this_path.blank? ? path_part.to_s : ",#{path_part}")
255
- unless (sub_next = sub_objects[this_path])
256
- # Check if we're hitting reference data / a lookup thing
257
- assoc = v.prefix_assocs[idx]
258
- # belongs_to some lookup (reference) data
259
- if assoc && reference_models.include?(assoc.class_name)
260
- lookup_match = assoc.klass.find_by(v.name => row[key])
261
- # Do a partial match if this column allows for it
262
- # and we only find one matching result.
263
- if lookup_match.nil? && partials.include?(v.titleize)
264
- lookup_match ||= assoc.klass.where("#{v.name} LIKE '#{row[key]}%'")
265
- lookup_match = (lookup_match.length == 1 ? lookup_match.first : nil)
266
- end
267
- sub_obj.send("#{path_part}=", lookup_match) unless lookup_match.nil?
268
- # Reference data from the public level means we stop here
269
- sub_obj = nil
270
- break
271
- end
272
- # Get existing related object, or create a new one
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?
275
- klass = Object.const_get(assoc&.class_name)
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)
307
- # Try to find a unique item if one is referenced
308
- # %%% There is possibility that when bringing in related classes using a nil
309
- # in IMPORT_TEMPLATE[:all] that this will break. Need to test deeply nested things.
310
- start = (v.pre_prefix.blank? ? 0 : v.pre_prefix.length)
311
- trim_prefix = v.titleize[start..-(v.name.length + 2)]
312
- trim_prefix << ' ' unless trim_prefix.blank?
313
- # assoc.inverse_of is the belongs_to side of the has_many train we came in here on.
314
- sub_hm, criteria = assoc.klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
315
- # If still not found then create a new related object using this has_many collection
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 }
344
- end
345
- unless sub_next.nil?
346
- # if sub_next.class.name == devise_class && # only for Devise users
347
- # sub_next.email =~ Devise.email_regexp
348
- # if existing.include?([sub_next.email])
349
- # User already exists
350
- # else
351
- # sub_next.invite!
352
- # end
353
- # end
354
- sub_objects[this_path] = sub_next if this_path.present?
355
- end
356
- end
357
- sub_obj = sub_next
358
- end
359
- next if sub_obj.nil?
115
+ ::DutyFree::Extensions.import(self, data, import_template)
116
+ end
360
117
 
361
- next unless sub_obj.respond_to?(sym = "#{v.name}=".to_sym)
118
+ private
362
119
 
363
- col_type = sub_obj.class.columns_hash[v.name.to_s]&.type
364
- if col_type.nil? && (virtual_columns = import_template[:virtual_columns]) &&
365
- (virtual_columns = virtual_columns[this_path] || virtual_columns)
366
- col_type = virtual_columns[v.name]
367
- end
368
- if col_type == :boolean
369
- if row[key].nil?
370
- # Do nothing when it's nil
371
- elsif %w[true t yes y].include?(row[key]&.strip&.downcase) # Used to cover 'on'
372
- row[key] = true
373
- elsif %w[false f no n].include?(row[key]&.strip&.downcase) # Used to cover 'off'
374
- row[key] = false
375
- else
376
- row_errors[v.name] ||= []
377
- row_errors[v.name] << "Boolean value \"#{row[key]}\" in column #{key + 1} not recognized"
378
- end
379
- end
120
+ def _defined_uniques(uniques, cols = [], col_list = nil, starred = [], trim_prefix = '')
121
+ col_list ||= cols.join('|')
122
+ unless (defined_uniq = (@defined_uniques ||= {})[col_list])
123
+ utilised = {} # Track columns that have been referenced thusfar
124
+ defined_uniq = uniques.each_with_object({}) do |unique, s|
125
+ if unique.is_a?(Array)
126
+ key = []
127
+ value = []
128
+ unique.each do |unique_part|
129
+ val = (unique_part_name = unique_part.to_s.titleize).start_with?(trim_prefix) &&
130
+ cols.index(upn = unique_part_name[trim_prefix.length..-1])
131
+ next unless val
380
132
 
381
- is_yet_changed = sub_obj.changed?
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?
387
- # else
388
- # puts " #{sub_obj.class.name} doesn't respond to #{sym}"
133
+ key << upn
134
+ value << val
389
135
  end
390
- # Try to save final sub-object(s) if any exist
391
- ::DutyFree::Extensions._save_pending(to_be_saved)
392
-
393
- # Reinstate any missing polymorphic _type and _id values
394
- polymorphics.each do |poly|
395
- if !poly[:parent].new_record? || poly[:parent].save
396
- poly[:child].send("#{poly[:type_col]}=".to_sym, poly[:parent].class.name)
397
- poly[:child].send("#{poly[:id_col]}=".to_sym, poly[:parent].id)
398
- end
136
+ unless key.empty?
137
+ s[key] = value
138
+ utilised[key] = nil
399
139
  end
400
-
401
- # Give a window of opportunity to tweak user objects controlled by Devise
402
- obj_class = obj.class
403
- is_do_save = if obj_class.respond_to?(:before_devise_save) && obj_class.name == devise_class
404
- obj_class.before_devise_save(obj, existing)
405
- else
406
- true
407
- end
408
-
409
- if obj.valid?
410
- obj.save if is_do_save
411
- # Snag back any changes to the unique columns. (For instance, Devise downcases email addresses.)
412
- existing_unique = valid_unique.keys.inject([]) { |s, v| s << obj.send(v).to_s }
413
- # Update the duplicate counts and inserted / updated results
414
- counts[existing_unique] << row_num
415
- (is_insert ? inserts : updates) << { row_num => existing_unique } if is_do_save
416
- # Track this new object so we can properly sense any duplicates later
417
- existing[existing_unique] = obj.id
418
- else
419
- row_errors.merge! obj.errors.messages
140
+ else
141
+ val = (unique_name = unique.to_s.titleize).start_with?(trim_prefix) &&
142
+ cols.index(un = unique_name[trim_prefix.length..-1])
143
+ if val
144
+ s[[un]] = [val]
145
+ utilised[[un]] = nil
420
146
  end
421
- errors << { row_num => row_errors } unless row_errors.empty?
422
147
  end
423
- end
424
- duplicates = counts.each_with_object([]) do |v, s|
425
- s += v.last[1..-1].map { |line_num| { line_num => v.first } } if v.last.count > 1
426
148
  s
427
149
  end
428
- ret = { inserted: inserts, updated: updates, duplicates: duplicates, errors: errors }
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)
440
- end
441
- end
442
- ret
443
- end
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
150
+ if defined_uniq.empty?
151
+ (starred - utilised.keys).each { |star| defined_uniq[[star]] = [cols.index(star)] }
152
+ # %%% puts "Tried to establish #{defined_uniq.inspect}"
478
153
  end
154
+ @defined_uniques[col_list] = defined_uniq
479
155
  end
480
-
481
- puts "* Wow, had no luck at all finding a foreign key for #{assoc.inspect}"
156
+ defined_uniq
482
157
  end
483
158
 
484
159
  # For use with importing, based on the provided column list calculate all valid combinations
485
160
  # of unique columns. If there is no valid combination, throws an error.
486
- # Returns an object found by this means.
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 = '')
161
+ # Returns an object found by this means, as well as the criteria that was used to find it.
162
+ def _find_existing(uniques, cols, starred, import_template, keepers, train_we_came_in_here_on,
163
+ row = nil, klass_or_collection = nil, template_all = nil, trim_prefix = '',
164
+ assoc = nil, base_obj = nil)
489
165
  unless trim_prefix.blank?
490
166
  cols = cols.map { |c| c.start_with?(trim_prefix) ? c[trim_prefix.length..-1] : nil }
491
167
  starred = starred.each_with_object([]) do |v, s|
@@ -536,7 +212,7 @@ module DutyFree
536
212
  # folks based on having their email as a unique identifier, and other folks by having a
537
213
  # combination of their name and street address as unique, and use both of those possible
538
214
  # 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)
215
+ all_vus = _defined_uniques(uniques, cols, col_list, starred, trim_prefix)
540
216
 
541
217
  # %%% Ultimately may consider making this recursive
542
218
  reflect_on_all_associations.each do |sn_bt|
@@ -662,13 +338,56 @@ module DutyFree
662
338
  end
663
339
 
664
340
  return uniq_lookups.merge(criteria) if only_valid_uniques
665
-
666
341
  # If there's nothing to match upon then we're out
667
342
  return [nil, {}] if new_criteria_all_nil
668
343
 
669
344
  # With this criteria, find any matching has_many row we can so we can update it.
670
345
  # First try directly looking it up through ActiveRecord.
671
- found_object = klass_or_collection.find_by(criteria)
346
+ klass_or_collection ||= self # Comes in as nil for has_one with no object yet attached
347
+ # HABTM proxy
348
+ # binding.pry if klass_or_collection.is_a?(ActiveRecord::Associations::CollectionProxy)
349
+ # if klass_or_collection.respond_to?(:proxy_association) && klass_or_collection.proxy_association.options.include?(:through)
350
+ # klass_or_collection.proxy_association.association_scope.to_a
351
+
352
+ # if assoc.respond_to?(:require_association) && klass_or_collection.is_a?(Array)
353
+ # # else
354
+ # # klass_or_collection = assoc.klass
355
+ # end
356
+ # end
357
+ found_object = case klass_or_collection
358
+ when ActiveRecord::Base # object from a has_one?
359
+ existing_object = klass_or_collection
360
+ klass_or_collection = klass_or_collection.class
361
+ other_object = klass_or_collection.find_by(criteria)
362
+ pk = klass_or_collection.primary_key
363
+ existing_object.send(pk) == other_object&.send(pk) ? existing_object : other_object
364
+ when Array # has_* in AR < 4.0
365
+ # Old AR doesn't have a CollectionProxy that can do any of this on its own.
366
+ base_id = base_obj.send(base_obj.class.primary_key)
367
+ if assoc.macro == :has_and_belongs_to_many || (assoc.macro == :has_many && assoc.options[:through])
368
+ # Find all association foreign keys, then find or create the foreign object
369
+ # based on criteria, and finally put an entry with both foreign keys into
370
+ # the associative table unless it already exists.
371
+ ajt = assoc.through_reflection&.table_name || assoc.join_table
372
+ fk = assoc.foreign_key
373
+ afk = assoc.association_foreign_key
374
+ existing_ids = ActiveRecord::Base.connection.execute(
375
+ "SELECT #{afk} FROM #{ajt} WHERE #{fk} = #{base_id}"
376
+ ).map { |r| r[afk] }
377
+ new_or_existing = assoc.klass.find_or_create_by(criteria)
378
+ new_or_existing_id = new_or_existing.send(new_or_existing.class.primary_key)
379
+ unless existing_ids.include?(new_or_existing_id)
380
+ ActiveRecord::Base.connection.execute(
381
+ "INSERT INTO #{ajt} (#{fk}, #{afk}) VALUES (#{base_id}, #{new_or_existing_id})"
382
+ )
383
+ end
384
+ new_or_existing
385
+ else # Must be a has_many
386
+ assoc.klass.find_or_create_by(criteria.merge({ assoc.foreign_key => base_id }))
387
+ end
388
+ else
389
+ klass_or_collection.find_by(criteria)
390
+ end
672
391
  # If not successful, such as when fields are exposed via helper methods instead of being
673
392
  # real columns in the database tables, try this more intensive approach. This is useful
674
393
  # if you had full name kind of data coming in on a spreadsheeet, but in the destination
@@ -692,58 +411,445 @@ module DutyFree
692
411
  # that match up to a primary key so that if needed a new related object can be built,
693
412
  # complete with all its association detail.
694
413
  [found_object, criteria.merge(bt_criteria)]
695
- end
414
+ end # _find_existing
415
+ end # module ClassMethods
696
416
 
697
- private
417
+ # With an array of incoming data, the first row having column names, perform the import
418
+ def self.import(obj_klass, data, import_template = nil)
419
+ instance_variable_set(:@defined_uniques, nil)
420
+ instance_variable_set(:@valid_uniques, nil)
421
+
422
+ import_template ||= if obj_klass.constants.include?(:IMPORT_TEMPLATE)
423
+ obj_klass::IMPORT_TEMPLATE
424
+ else
425
+ obj_klass.suggest_template(0, false, false)
426
+ end
427
+ # puts "Chose #{import_template}"
428
+ inserts = []
429
+ updates = []
430
+ counts = Hash.new { |h, k| h[k] = [] }
431
+ errors = []
432
+
433
+ is_first = true
434
+ uniques = nil
435
+ cols = nil
436
+ starred = []
437
+ partials = []
438
+ # See if we can find the model if given only a string
439
+ if obj_klass.is_a?(String)
440
+ obj_klass_camelized = obj_klass.camelize.singularize
441
+ obj_klass = Object.const_get(obj_klass_camelized) if Object.const_defined?(obj_klass_camelized)
442
+ end
443
+ table_name = obj_klass.table_name unless obj_klass.is_a?(String)
444
+ is_build_table = if import_template.include?(:all!)
445
+ # Search for presence of this table
446
+ table_name = obj_klass.underscore.pluralize if obj_klass.is_a?(String)
447
+ !ActiveRecord::ConnectionAdapters::SchemaStatements.table_exists?(table_name)
448
+ else
449
+ false
450
+ end
451
+ # If we do need to build the table then we require an :all!, otherwise in the case that the table
452
+ # does not need to be built or was already built then try :all, and fall back on :all!.
453
+ all = import_template[is_build_table ? :all! : :all] || import_template[:all!]
454
+ keepers = {}
455
+ valid_unique = nil
456
+ existing = {}
457
+ devise_class = ''
458
+ ret = nil
459
+
460
+ # Multi-tenancy gem Apartment can be used if there are separate schemas per tenant
461
+ reference_models = if Object.const_defined?('Apartment')
462
+ Apartment.excluded_models
463
+ else
464
+ []
465
+ end
466
+
467
+ if Object.const_defined?('Devise')
468
+ Object.const_get('Devise') # If this fails, devise_class will remain a blank string.
469
+ devise_class = Devise.mappings.values.first.class_name
470
+ reference_models -= [devise_class]
471
+ else
472
+ devise_class = ''
473
+ end
698
474
 
699
- def defined_uniques(uniques, cols = [], col_list = nil, starred = [], trim_prefix = '')
700
- col_list ||= cols.join('|')
701
- unless (defined_uniq = (@defined_uniques ||= {})[col_list])
702
- utilised = {} # Track columns that have been referenced thusfar
703
- defined_uniq = uniques.each_with_object({}) do |unique, s|
704
- if unique.is_a?(Array)
705
- key = []
706
- value = []
707
- unique.each do |unique_part|
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
475
+ # Did they give us a filename?
476
+ if data.is_a?(String)
477
+ # Filenames with full paths can not be longer than 4096 characters, and can not
478
+ # include newline characters
479
+ data = if data.length <= 4096 && !data.index('\n')
480
+ File.open(data)
481
+ else
482
+ # Any multi-line string is likely CSV data
483
+ # %%% Test to see if TAB characters are present on the first line, instead of commas
484
+ CSV.new(data)
485
+ end
486
+ end
487
+ # Or perhaps us a file?
488
+ if data.is_a?(File)
489
+ # Use the "roo" gem if it's available
490
+ # When we're ready to try parsing this thing on our own, shared strings and all, then use
491
+ # the rubyzip gem also along with it:
492
+ # https://github.com/rubyzip/rubyzip
493
+ # require 'zip'
494
+ data = if Object.const_defined?('Roo::Spreadsheet', { csv_options: { encoding: 'bom|utf-8' } })
495
+ Roo::Spreadsheet.open(data)
496
+ else
497
+ # Otherwise generic CSV parsing
498
+ require 'csv' unless Object.const_defined?('CSV')
499
+ CSV.open(data)
500
+ end
501
+ end
711
502
 
712
- key << upn
713
- value << val
503
+ # Will show as just one transaction when using auditing solutions such as PaperTrail
504
+ ActiveRecord::Base.transaction do
505
+ # Check to see if they want to do anything before the whole import
506
+ # First if defined in the import_template, then if there is a method in the class,
507
+ # and finally (not yet implemented) a generic global before_import
508
+ my_before_import = import_template[:before_import]
509
+ my_before_import ||= respond_to?(:before_import) && method(:before_import)
510
+ # my_before_import ||= some generic my_before_import
511
+ if my_before_import
512
+ last_arg_idx = my_before_import.parameters.length - 1
513
+ arguments = [data, import_template][0..last_arg_idx]
514
+ data = ret if (ret = my_before_import.call(*arguments)).is_a?(Enumerable)
515
+ end
516
+ build_tables = nil
517
+ data.each_with_index do |row, row_num|
518
+ row_errors = {}
519
+ if is_first # Anticipate that first row has column names
520
+ uniques = import_template[:uniques]
521
+
522
+ # Look for UTF-8 BOM in very first cell
523
+ row[0] = row[0][3..-1] if row[0].start_with?([239, 187, 191].pack('U*'))
524
+ # How about a first character of FEFF or FFFE to support UTF-16 BOMs?
525
+ # FE FF big-endian (standard)
526
+ # FF FE little-endian
527
+ row[0] = row[0][1..-1] if [65_279, 65_534].include?(row[0][0].ord)
528
+ cols = row.map { |col| (col || '').strip }
529
+
530
+ # Unique column combinations can be called out explicitly in models using uniques: {...}, or to just
531
+ # define one column at a time simply mark with an asterisk.
532
+ # Track and clean up stars
533
+ starred = cols.select do |col|
534
+ if col[0] == '*'
535
+ col.slice!(0)
536
+ col.strip!
714
537
  end
715
- unless key.empty?
716
- s[key] = value
717
- utilised[key] = nil
538
+ end
539
+ partials = cols.select do |col|
540
+ if col[0] == '~'
541
+ col.slice!(0)
542
+ col.strip!
718
543
  end
544
+ end
545
+ cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) }
546
+ obj_klass.send(:_defined_uniques, uniques, cols, cols.join('|'), starred)
547
+ # Main object asking for table to be built?
548
+ build_tables = {}
549
+ build_tables[path_name] = [namespaces, is_need_model, is_need_table] if is_build_table
550
+ # Make sure that at least half of them match what we know as being good column names
551
+ template_column_objects = ::DutyFree::Extensions._recurse_def(obj_klass, import_template[:all], import_template, build_tables).first
552
+ cols.each_with_index do |col, idx|
553
+ # prefixes = col_detail.pre_prefix + (col_detail.prefix.blank? ? [] : [col_detail.prefix])
554
+ # %%% Would be great here if when one comes back nil, try to find the closest match
555
+ # and indicate to the user a "did you mean?" about it.
556
+ keepers[idx] = template_column_objects.find { |col_obj| col_obj.titleize == col }
557
+ # puts "Could not find a match for column #{idx + 1}, #{col}" if keepers[idx].nil?
558
+ end
559
+ raise ::DutyFree::LessThanHalfAreMatchingColumnsError, I18n.t('import.altered_import_template_coumns') if keepers.length < (cols.length / 2) - 1
560
+
561
+ # Returns just the first valid unique lookup set if there are multiple
562
+ valid_unique = obj_klass.send(:_find_existing, uniques, cols, starred, import_template, keepers, false)
563
+ # Make a lookup from unique values to specific IDs
564
+ existing = obj_klass.pluck(*([:id] + valid_unique.keys)).each_with_object(existing) do |v, s|
565
+ s[v[1..-1].map(&:to_s)] = v.first
566
+ s
567
+ end
568
+ is_first = false
569
+ else # Normal row of data
570
+ is_insert = false
571
+ existing_unique = valid_unique.inject([]) do |s, v|
572
+ s << if v.last.is_a?(Array)
573
+ v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck(MAX_ID).first.to_s
574
+ else
575
+ row[v.last].to_s
576
+ end
577
+ end
578
+ to_be_saved = []
579
+ # Check to see if they want to preprocess anything
580
+ existing_unique = @before_process.call(valid_unique, existing_unique) if @before_process ||= import_template[:before_process]
581
+ if (criteria = existing[existing_unique])
582
+ obj = obj_klass.find(criteria)
719
583
  else
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
584
+ is_insert = true
585
+ # unless build_tables.empty? # include?()
586
+ # binding.pry
587
+ # x = 5
588
+ # end
589
+ to_be_saved << [obj = obj_klass.new]
590
+ end
591
+ sub_obj = nil
592
+ polymorphics = []
593
+ sub_objects = {}
594
+ this_path = nil
595
+ keepers.each do |key, v|
596
+ next if v.nil?
597
+
598
+ sub_obj = obj
599
+ this_path = +''
600
+ # puts "p: #{v.path}"
601
+ v.path.each_with_index do |path_part, idx|
602
+ this_path << (this_path.blank? ? path_part.to_s : ",#{path_part}")
603
+ unless (sub_next = sub_objects[this_path])
604
+ # Check if we're hitting reference data / a lookup thing
605
+ assoc = v.prefix_assocs[idx]
606
+ # belongs_to some lookup (reference) data
607
+ if assoc && reference_models.include?(assoc.class_name)
608
+ lookup_match = assoc.klass.find_by(v.name => row[key])
609
+ # Do a partial match if this column allows for it
610
+ # and we only find one matching result.
611
+ if lookup_match.nil? && partials.include?(v.titleize)
612
+ lookup_match ||= assoc.klass.where("#{v.name} LIKE '#{row[key]}%'")
613
+ lookup_match = (lookup_match.length == 1 ? lookup_match.first : nil)
614
+ end
615
+ sub_obj.send("#{path_part}=", lookup_match) unless lookup_match.nil?
616
+ # Reference data from the public level means we stop here
617
+ sub_obj = nil
618
+ break
619
+ end
620
+ # Get existing related object, or create a new one
621
+ # This first part works for belongs_to. has_many and has_one get sorted below.
622
+ # start = (v.pre_prefix.blank? ? 0 : v.pre_prefix.length)
623
+ start = 0
624
+ trim_prefix = v.titleize[start..-(v.name.length + 2)]
625
+ trim_prefix << ' ' unless trim_prefix.blank?
626
+ if (sub_next = sub_obj.send(path_part)).nil? && assoc.belongs_to?
627
+ klass = Object.const_get(assoc&.class_name)
628
+ # Try to find a unique item if one is referenced
629
+ sub_next = nil
630
+ begin
631
+ sub_next, criteria = klass.send(:_find_existing, uniques, cols, starred, import_template, keepers, nil, row, klass, all, trim_prefix, assoc)
632
+ rescue ::DutyFree::NoUniqueColumnError
633
+ end
634
+ # puts "#{v.path} #{criteria.inspect}"
635
+ bt_name = "#{path_part}="
636
+ unless sub_next || (klass == sub_obj.class && criteria.empty?)
637
+ sub_next = klass.new(criteria || {})
638
+ to_be_saved << [sub_next, sub_obj, bt_name, sub_next]
639
+ end
640
+ # This wires it up in memory, but doesn't yet put the proper foreign key ID in
641
+ # place when the primary object is a new one (and thus not having yet been saved
642
+ # doesn't yet have an ID).
643
+ # binding.pry if !sub_next.new_record? && sub_next.name == 'Squidget Widget' # !sub_obj.changed?
644
+ # # %%% Question is -- does the above set changed? when a foreign key is not yet set
645
+ # # and only the in-memory object has changed?
646
+ is_yet_changed = sub_obj.changed?
647
+ sub_obj.send(bt_name, sub_next)
648
+
649
+ # We need this in the case of updating the primary object across a belongs_to when
650
+ # the foreign one already exists and is not otherwise changing, such as when it is
651
+ # not a new one, so wouldn't otherwise be getting saved.
652
+ to_be_saved << [sub_obj] if !sub_obj.new_record? && !is_yet_changed && sub_obj.changed?
653
+ # From a has_many or has_one?
654
+ # Rails 4.0 and later can do: sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
655
+ elsif [:has_many, :has_one, :has_and_belongs_to_many].include?(assoc.macro) # && !assoc.options[:through]
656
+ ::DutyFree::Extensions._save_pending(to_be_saved)
657
+ # Try to find a unique item if one is referenced
658
+ # %%% There is possibility that when bringing in related classes using a nil
659
+ # in IMPORT_TEMPLATE[:all] that this will break. Need to test deeply nested things.
660
+
661
+ # assoc.inverse_of is the belongs_to side of the has_many train we came in here on.
662
+ sub_hm, criteria = assoc.klass.send(:_find_existing, uniques, cols, starred, import_template, keepers, assoc.inverse_of,
663
+ row, sub_next, all, trim_prefix, assoc,
664
+ # Just in case we're running Rails < 4.0 and this is a haas_*
665
+ sub_obj)
666
+ # If still not found then create a new related object using this has_many collection
667
+ # (criteria.empty? ? nil : sub_next.new(criteria))
668
+ if sub_hm
669
+ sub_next = sub_hm
670
+ elsif assoc.macro == :has_one
671
+ # assoc.active_record.name.underscore is only there to support older Rails
672
+ # that doesn't do automatic inverse_of
673
+ ho_name = "#{assoc.inverse_of&.name || assoc.active_record.name.underscore}="
674
+ sub_next = assoc.klass.new(criteria)
675
+ to_be_saved << [sub_next, sub_next, ho_name, sub_obj]
676
+ elsif assoc.macro == :has_and_belongs_to_many ||
677
+ (assoc.macro == :has_many && assoc.options[:through])
678
+ # sub_next = sub_next.new(criteria)
679
+ # Search for one to wire up if it might already exist, otherwise create one
680
+ sub_next = assoc.klass.find_by(criteria) || assoc.klass.new(criteria)
681
+ to_be_saved << [sub_next, :has_and_belongs_to_many, assoc.name, sub_obj]
682
+ else
683
+ # Two other methods that are possible to check for here are :conditions and
684
+ # :sanitized_conditions, which do not exist in Rails 4.0 and later.
685
+ sub_next = if assoc.respond_to?(:require_association)
686
+ # With Rails < 4.0, sub_next could come in as an Array or a broken CollectionProxy
687
+ assoc.klass.new({ ::DutyFree::Extensions._fk_from(assoc) => sub_obj.send(sub_obj.class.primary_key) }.merge(criteria))
688
+ else
689
+ sub_next.proxy_association.reflection.instance_variable_set(:@foreign_key, ::DutyFree::Extensions._fk_from(assoc))
690
+ sub_next.new(criteria)
691
+ end
692
+ to_be_saved << [sub_next]
693
+ end
694
+ # else
695
+ # belongs_to for a found object, or a HMT
696
+ end
697
+ # # Incompatible with Rails < 4.2
698
+ # # Look for possible missing polymorphic detail
699
+ # # Maybe can test for this via assoc.through_reflection
700
+ # if assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) &&
701
+ # (delegate = assoc.send(:delegate_reflection)&.active_record&.reflect_on_association(assoc.source_reflection_name)) &&
702
+ # delegate.options[:polymorphic]
703
+ # polymorphics << { parent: sub_next, child: sub_obj, type_col: delegate.foreign_type, id_col: delegate.foreign_key.to_s }
704
+ # end
705
+
706
+ # rubocop:disable Style/SoleNestedConditional
707
+ unless sub_next.nil?
708
+ # if sub_next.class.name == devise_class && # only for Devise users
709
+ # sub_next.email =~ Devise.email_regexp
710
+ # if existing.include?([sub_next.email])
711
+ # User already exists
712
+ # else
713
+ # sub_next.invite!
714
+ # end
715
+ # end
716
+ sub_objects[this_path] = sub_next if this_path.present?
717
+ end
718
+ # rubocop:enable Style/SoleNestedConditional
719
+ end
720
+ sub_obj = sub_next
721
+ end
722
+ next if sub_obj.nil?
723
+
724
+ next unless sub_obj.respond_to?(sym = "#{v.name}=".to_sym)
725
+
726
+ col_type = sub_obj.class.columns_hash[v.name.to_s]&.type
727
+ if col_type.nil? && (virtual_columns = import_template[:virtual_columns]) &&
728
+ (virtual_columns = virtual_columns[this_path] || virtual_columns)
729
+ col_type = virtual_columns[v.name]
725
730
  end
731
+ if col_type == :boolean
732
+ if row[key].nil?
733
+ # Do nothing when it's nil
734
+ elsif %w[true t yes y].include?(row[key]&.strip&.downcase) # Used to cover 'on'
735
+ row[key] = true
736
+ elsif %w[false f no n].include?(row[key]&.strip&.downcase) # Used to cover 'off'
737
+ row[key] = false
738
+ else
739
+ row_errors[v.name] ||= []
740
+ row_errors[v.name] << "Boolean value \"#{row[key]}\" in column #{key + 1} not recognized"
741
+ end
742
+ end
743
+
744
+ is_yet_changed = sub_obj.changed?
745
+ sub_obj.send(sym, row[key])
746
+ # If this one is transitioning from having not changed to now having been changed,
747
+ # and is not a new one anyway that would already be lined up to get saved, then we
748
+ # mark it to now be saved.
749
+ to_be_saved << [sub_obj] if !sub_obj.new_record? && !is_yet_changed && sub_obj.changed?
750
+ # else
751
+ # puts " #{sub_obj.class.name} doesn't respond to #{sym}"
726
752
  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}"
753
+ # Try to save final sub-object(s) if any exist
754
+ ::DutyFree::Extensions._save_pending(to_be_saved)
755
+
756
+ # Reinstate any missing polymorphic _type and _id values
757
+ polymorphics.each do |poly|
758
+ if !poly[:parent].new_record? || poly[:parent].save
759
+ poly[:child].send("#{poly[:type_col]}=".to_sym, poly[:parent].class.name)
760
+ poly[:child].send("#{poly[:id_col]}=".to_sym, poly[:parent].id)
761
+ end
762
+ end
763
+
764
+ # Give a window of opportunity to tweak user objects controlled by Devise
765
+ obj_class = obj.class
766
+ is_do_save = if obj_class.respond_to?(:before_devise_save) && obj_class.name == devise_class
767
+ obj_class.before_devise_save(obj, existing)
768
+ else
769
+ true
770
+ end
771
+
772
+ if obj.valid?
773
+ obj.save if is_do_save
774
+ # Snag back any changes to the unique columns. (For instance, Devise downcases email addresses.)
775
+ existing_unique = valid_unique.keys.inject([]) { |s, v| s << obj.send(v).to_s }
776
+ # Update the duplicate counts and inserted / updated results
777
+ counts[existing_unique] << row_num
778
+ (is_insert ? inserts : updates) << { row_num => existing_unique } if is_do_save
779
+ # Track this new object so we can properly sense any duplicates later
780
+ existing[existing_unique] = obj.id
781
+ else
782
+ row_errors.merge! obj.errors.messages
783
+ end
784
+ errors << { row_num => row_errors } unless row_errors.empty?
732
785
  end
733
- @defined_uniques[col_list] = defined_uniq
734
786
  end
735
- defined_uniq
787
+ duplicates = counts.each_with_object([]) do |v, s|
788
+ s += v.last[1..-1].map { |line_num| { line_num => v.first } } if v.last.count > 1
789
+ s
790
+ end
791
+ ret = { inserted: inserts, updated: updates, duplicates: duplicates, errors: errors }
792
+
793
+ # Check to see if they want to do anything after the import
794
+ # First if defined in the import_template, then if there is a method in the class,
795
+ # and finally (not yet implemented) a generic global after_import
796
+ my_after_import = import_template[:after_import]
797
+ my_after_import ||= respond_to?(:after_import) && method(:after_import)
798
+ # my_after_import ||= some generic my_after_import
799
+ if my_after_import
800
+ last_arg_idx = my_after_import.parameters.length - 1
801
+ arguments = [ret][0..last_arg_idx]
802
+ # rubocop:disable Lint/UselessAssignment
803
+ ret = ret2 if (ret2 = my_after_import.call(*arguments)).is_a?(Hash)
804
+ # rubocop:enable Lint/UselessAssignment
805
+ end
736
806
  end
737
- end # module ClassMethods
807
+ ret
808
+ end
809
+
810
+ def self._fk_from(assoc)
811
+ # Try first to trust whatever they've marked as being the foreign_key, and then look
812
+ # at the inverse's foreign key setting if available. In all cases don't accept
813
+ # anything that's not backed with a real column in the table.
814
+ col_names = assoc.klass.column_names
815
+ if (
816
+ (fk_name = assoc.options[:foreign_key]) ||
817
+ (fk_name = assoc.inverse_of&.options&.fetch(:foreign_key) { nil }) ||
818
+ (assoc.respond_to?(:foreign_key) && (fk_name = assoc.foreign_key)) ||
819
+ (fk_name = assoc.inverse_of&.foreign_key) ||
820
+ (fk_name = assoc.inverse_of&.association_foreign_key)
821
+ ) && col_names.include?(fk_name.to_s)
822
+ return fk_name
823
+ end
824
+
825
+ # Don't let this fool you -- we really are in search of the foreign key name here,
826
+ # and Rails 3.0 and older used some fairly interesting conventions, calling it instead
827
+ # the "primary_key_name"!
828
+ if assoc.respond_to?(:primary_key_name)
829
+ if (fk_name = assoc.primary_key_name) && col_names.include?(fk_name.to_s)
830
+ return fk_name
831
+ end
832
+ if (fk_name = assoc.inverse_of.primary_key_name) && col_names.include?(fk_name.to_s)
833
+ return fk_name
834
+ end
835
+ end
836
+
837
+ puts "* Wow, had no luck at all finding a foreign key for #{assoc.inspect}"
838
+ end
839
+
840
+ # unless build_tables.include?((foreign_class = tbs[2].class).underscore)
841
+ # build_table(build_tables, tbs[1].class, foreign_class, :has_one)
738
842
 
739
843
  # Called before building any object linked through a has_one or has_many so that foreign key IDs
740
844
  # can be added properly to those new objects. Finally at the end also called to save everything.
741
845
  def self._save_pending(to_be_saved)
742
846
  while (tbs = to_be_saved.pop)
743
847
  # 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]
848
+ # Wire this one up if it had come from a has_one
849
+ if tbs[0] == tbs[1]
850
+ tbs[1].class.has_one(tbs[2]) unless tbs[1].respond_to?(tbs[2])
851
+ tbs[1].send(tbs[2], tbs[3])
852
+ end
747
853
 
748
854
  ais = (tbs.first.class.respond_to?(:around_import_save) && tbs.first.class.method(:around_import_save)) ||
749
855
  (respond_to?(:around_import_save) && method(:around_import_save))
@@ -764,7 +870,17 @@ module DutyFree
764
870
  tbs[0] == tbs[1] || # From a has_one?
765
871
  tbs.first.new_record?
766
872
 
767
- tbs[1].send(tbs[2], tbs[3])
873
+ if tbs[1] == :has_and_belongs_to_many # also used by has_many :through associations
874
+ collection = tbs[3].send(tbs[2])
875
+ being_shoveled_id = tbs[0].send(tbs[0].class.primary_key)
876
+ if collection.empty? ||
877
+ !collection.pluck("#{(klass = collection.first.class).table_name}.#{klass.primary_key}").include?(being_shoveled_id)
878
+ collection << tbs[0]
879
+ # puts collection.inspect
880
+ end
881
+ else # Traditional belongs_to
882
+ tbs[1].send(tbs[2], tbs[3])
883
+ end
768
884
  end
769
885
  end
770
886
 
@@ -783,40 +899,71 @@ module DutyFree
783
899
  template_detail_columns
784
900
  end
785
901
 
786
- # Recurse and return two arrays -- one with all columns in sequence, and one a hierarchy of
787
- # nested hashes to be used with ActiveRecord's .joins() to facilitate export.
788
- def self._recurse_def(klass, array, import_template, order_by = [], assocs = [], joins = [], pre_prefix = '', prefix = '')
902
+ # Recurse and return three arrays -- one with all columns in sequence, and one a hierarchy of
903
+ # nested hashes to be used with ActiveRecord's .joins() to facilitate export, and finally
904
+ # one that lists tables that need to be built along the way.
905
+ def self._recurse_def(klass, cols, import_template, build_tables = nil, order_by = nil, assocs = [], joins = [], pre_prefix = '', prefix = '')
906
+ prefix = prefix[0..-2] if (is_build_table = prefix.end_with?('!'))
789
907
  prefixes = ::DutyFree::Util._prefix_join([pre_prefix, prefix])
790
- # Confirm we can actually navigate through this association
791
- prefix_assoc = (assocs.last&.klass || klass).reflect_on_association(prefix) if prefix.present?
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]
908
+
909
+ if prefix.present?
910
+ # An indication to build this table and model if it doesn't exist?
911
+ if is_build_table && build_tables
912
+ namespaces = prefix.split('::')
913
+ model_name = namespaces.map { |part| part.singularize.camelize }.join('::')
914
+ prefix = namespaces.last
915
+ # %%% If the model already exists, take belongs_to cues from it for building the table
916
+ if (is_need_model = Object.const_defined?(model_name)) &&
917
+ (is_need_table = ActiveRecord::ConnectionAdapters::SchemaStatements.table_exists?(
918
+ (path_name = ::DutyFree::Util._prefix_join([prefixes, prefix.pluralize]))
919
+ ))
920
+ is_build_table = false
921
+ else
922
+ build_tables[path_name] = [namespaces, is_need_model, is_need_table] unless build_tables.include?(path_name)
923
+ end
924
+ end
925
+ unless is_build_table && build_tables
926
+ # Confirm we can actually navigate through this association
927
+ prefix_assoc = (assocs.last&.klass || klass).reflect_on_association(prefix) if prefix.present?
928
+ if prefix_assoc
929
+ assocs = assocs.dup << prefix_assoc
930
+ if order_by && [:has_many, :has_and_belongs_to_many].include?(prefix_assoc.macro) &&
931
+ (pk = prefix_assoc.active_record.primary_key)
932
+ order_by << ["#{prefixes.tr('.', '_')}_", pk]
933
+ end
934
+ end
796
935
  end
797
936
  end
798
- array = array.inject([]) do |s, col|
937
+ cols = cols.inject([]) do |s, col|
799
938
  s + if col.is_a?(Hash)
800
939
  col.inject([]) do |s2, v|
801
- joins << { v.first.to_sym => (joins_array = []) }
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
940
+ if order_by
941
+ # Find what the type is for this guy
942
+ next_klass = (assocs.last&.klass || klass).reflect_on_association(v.first)&.klass
943
+ # Used to be: { v.first.to_sym => (joins_array = []) }
944
+ joins << { v.first.to_sym => (joins_array = [next_klass]) }
945
+ end
946
+ s2 + _recurse_def(klass, (v.last.is_a?(Array) ? v.last : [v.last]), import_template, build_tables, order_by, assocs, joins_array, prefixes, v.first.to_sym).first
803
947
  end
804
948
  elsif col.nil?
805
949
  if assocs.empty?
806
950
  []
807
951
  else
808
952
  # Bring in from another class
809
- joins << { prefix => (joins_array = []) }
953
+ # Used to be: { prefix => (joins_array = []) }
954
+ # %%% Probably need a next_klass thing like above
955
+ joins << { prefix => (joins_array = [klass]) } if order_by
810
956
  # %%% Also bring in uniques and requireds
811
- _recurse_def(klass, assocs.last.klass::IMPORT_TEMPLATE[:all], import_template, order_by, assocs, joins_array, prefixes).first
957
+ _recurse_def(klass, assocs.last.klass::IMPORT_TEMPLATE[:all], import_template, build_tables, order_by, assocs, joins_array, prefixes).first
812
958
  end
813
959
  else
814
960
  [::DutyFree::Column.new(col, pre_prefix, prefix, assocs, klass, import_template[:as])]
815
961
  end
816
962
  end
817
- [array, joins]
963
+ [cols, joins]
818
964
  end
819
965
  end # module Extensions
966
+ # rubocop:enable Style/CommentedKeyword
820
967
 
821
968
  # Rails < 4.0 doesn't have ActiveRecord::RecordNotUnique, so use the more generic ActiveRecord::ActiveRecordError instead
822
969
  ar_not_unique_error = ActiveRecord.const_defined?('RecordNotUnique') ? ActiveRecord::RecordNotUnique : ActiveRecord::ActiveRecordError