duty_free 1.0.3 → 1.0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4451cbb29d90fb32347b1ff91e91a249da97769e7a64f2524efa912b8971bf86
4
- data.tar.gz: ee3b06059aa89be48329ef48605287e8cb1725dd9f61460d9b3d044354405540
3
+ metadata.gz: 714b772aa09eb15409cd7c58d4ccb64f967caf41f2a13e7813e90cc94d7eb48f
4
+ data.tar.gz: 750dfe36b729979a0356f14f8588568822ae22911c142d294358231504daa78e
5
5
  SHA512:
6
- metadata.gz: 953bb38945daf16d0f22daa921ad1e0f46881583d51145a59b1983869da8d79c0675640677f1774dbdb9321695aeb69dc29dd4f66250704be0199fed33ab84b6
7
- data.tar.gz: 8e3a0c166acb9bc22919218764e1e39ffa9d495e22ea93eaeb8ef6810b545a99f6af08bf594cc3958e7d9169ccfe5a7841c08b2799e4981066cb47968daaf26b
6
+ metadata.gz: 9d77a228b4e4990dad39882a4e67d1009f3c16114bc0e9b24280e1f8e9437c7fc55a8884eb37e8f6c15898958833c7f79bb7984d721f4dbd1fce7311ca6421d0
7
+ data.tar.gz: 1d35f3197ab500a6210eecc561190c02a04d87e3d4b2634a9a62695488d4e2ed0825f7240836aa9d32fe8ce01e06be38ddccda1955ca3d3669fd7ad18e8335f2
@@ -1,5 +1,69 @@
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 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
+ # 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 ActiveRecord < 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
+
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
+
3
67
  require 'active_record'
4
68
 
5
69
  require 'duty_free/config'
@@ -62,25 +126,301 @@ module DutyFree
62
126
  end
63
127
  end
64
128
 
129
+ # Major compatibility fixes for ActiveRecord < 4.2
130
+ # ================================================
65
131
  ActiveSupport.on_load(:active_record) do
132
+ # Rails < 4.0 cannot do #find_by, #find_or_create_by, or do #pluck on multiple columns, so here are the patches:
133
+ if ActiveRecord.version < ::Gem::Version.new('4.0')
134
+ module ActiveRecord
135
+ # Normally find_by is in FinderMethods, which older AR doesn't have
136
+ module Calculations
137
+ def find_by(*args)
138
+ where(*args).limit(1).to_a.first
139
+ end
140
+
141
+ def find_or_create_by(attributes, &block)
142
+ find_by(attributes) || create(attributes, &block)
143
+ end
144
+
145
+ def pluck(*column_names)
146
+ column_names.map! do |column_name|
147
+ if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s)
148
+ "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
149
+ else
150
+ column_name
151
+ end
152
+ end
153
+
154
+ # Same as: if has_include?(column_names.first)
155
+ if eager_loading? || (includes_values.present? && (column_names.first || references_eager_loaded_tables?))
156
+ construct_relation_for_association_calculations.pluck(*column_names)
157
+ else
158
+ relation = clone # spawn
159
+ relation.select_values = column_names
160
+ result = if klass.connection.class.name.end_with?('::PostgreSQLAdapter')
161
+ rslt = klass.connection.execute(relation.arel.to_sql)
162
+ rslt.type_map =
163
+ @type_map ||= proc do
164
+ # This aliasing avoids the warning:
165
+ # "no type cast defined for type "numeric" with oid 1700. Please cast this type
166
+ # explicitly to TEXT to be safe for future changes."
167
+ PG::BasicTypeRegistry.alias_type(0, 'numeric', 'text') # oid 1700
168
+ PG::BasicTypeRegistry.alias_type(0, 'time', 'text') # oid 1083
169
+ PG::BasicTypeMapForResults.new(klass.connection.raw_connection)
170
+ end.call
171
+ rslt.to_a
172
+ elsif respond_to?(:bind_values)
173
+ klass.connection.select_all(relation.arel, nil, bind_values)
174
+ else
175
+ klass.connection.select_all(relation.arel.to_sql, nil)
176
+ end
177
+ if result.empty?
178
+ []
179
+ else
180
+ columns = result.first.keys.map do |key|
181
+ # rubocop:disable Style/SingleLineMethods Naming/MethodParameterName
182
+ klass.columns_hash.fetch(key) do
183
+ Class.new { def type_cast(v); v; end }.new
184
+ end
185
+ # rubocop:enable Style/SingleLineMethods Naming/MethodParameterName
186
+ end
187
+
188
+ result = result.map do |attributes|
189
+ values = klass.initialize_attributes(attributes).values
190
+
191
+ columns.zip(values).map do |column, value|
192
+ column.type_cast(value)
193
+ end
194
+ end
195
+ columns.one? ? result.map!(&:first) : result
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ unless Base.is_a?(Calculations)
202
+ class Base
203
+ class << self
204
+ delegate :pluck, :find_by, :find_or_create_by, to: :scoped
205
+ end
206
+ end
207
+ end
208
+
209
+ # ActiveRecord < 3.2 doesn't have initialize_attributes, used by .pluck()
210
+ unless AttributeMethods.const_defined?('Serialization')
211
+ class Base
212
+ class << self
213
+ def initialize_attributes(attributes, options = {}) #:nodoc:
214
+ serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
215
+ # super(attributes, options)
216
+
217
+ serialized_attributes.each do |key, coder|
218
+ attributes[key] = Attribute.new(coder, attributes[key], serialized) if attributes.key?(key)
219
+ end
220
+
221
+ attributes
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ # This only gets added for ActiveRecord < 3.2
228
+ module Reflection
229
+ unless AssociationReflection.instance_methods.include?(:foreign_key)
230
+ class AssociationReflection < MacroReflection
231
+ alias foreign_key association_foreign_key
232
+ end
233
+ end
234
+ end
235
+
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?
249
+
250
+ relation = clone
251
+ relation.send(:apply_modules, modules.flatten)
252
+ relation
253
+ end
254
+ end
255
+ end
256
+
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
274
+
275
+ self.default_scopes += [scope]
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
283
+
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
312
+ end
313
+
314
+ unless DateTime.instance_methods.include?(:nsec)
315
+ class DateTime < Date
316
+ def nsec
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
334
+ end
335
+ end
336
+ end
337
+
66
338
  include ::DutyFree::Extensions
67
339
  end
68
340
 
69
- # # Require frameworks
70
- # if defined?(::Rails)
71
- # # Rails module is sometimes defined by gems like rails-html-sanitizer
72
- # # so we check for presence of Rails.application.
73
- # if defined?(::Rails.application)
74
- # require "duty_free/frameworks/rails"
75
- # else
76
- # ::Kernel.warn(<<-EOS.freeze
77
- # DutyFree has been loaded too early, before rails is loaded. This can
78
- # happen when another gem defines the ::Rails namespace, then DF is loaded,
79
- # all before rails is loaded. You may want to reorder your Gemfile, or defer
80
- # the loading of DF by using `require: false` and a manual require elsewhere.
81
- # EOS
82
- # )
83
- # end
84
- # else
85
- # require "duty_free/frameworks/active_record"
86
- # 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,20 +36,16 @@ module DutyFree
36
36
  end
37
37
 
38
38
  def titleize
39
- @titleized ||= sym_string.titleize
39
+ @titleize ||= 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
52
- @sym_string ||= ::DutyFree::Util._prefix_join(
47
+ def to_sym
48
+ @to_sym ||= ::DutyFree::Util._prefix_join(
53
49
  [pre_prefix, prefix, ::DutyFree::Util._clean_name(name, import_template_as)],
54
50
  '_'
55
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,12 +17,9 @@ 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
- def df_export(is_with_data = true, import_template = nil)
21
+ def df_export(is_with_data = true, import_template = nil, use_inner_joins = false)
22
+ use_inner_joins = true unless respond_to?(:left_joins)
23
23
  # In case they are only supplying the columns hash
24
24
  if is_with_data.is_a?(Hash) && !import_template
25
25
  import_template = is_with_data
@@ -50,17 +50,51 @@ module DutyFree
50
50
  rows = [rows]
51
51
 
52
52
  if is_with_data
53
+ order_by = []
54
+ order_by << ['_', primary_key] if primary_key
55
+ all = import_template[:all] || import_template[:all!]
53
56
  # Automatically create a JOINs strategy and select list to get back all related rows
54
- template_cols, template_joins = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template)
55
- relation = left_joins(template_joins)
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)
61
+ relation = use_inner_joins ? joins(template_joins) : left_joins(template_joins)
56
62
 
57
63
  # So we can properly create the SELECT list, create a mapping between our
58
64
  # column alias prefixes and the aliases AREL creates.
59
- arel_alias_names = ::DutyFree::Util._recurse_arel(relation.arel.ast.cores.first.source)
60
- our_names = ::DutyFree::Util._recurse_arel(template_joins)
61
- mapping = our_names.zip(arel_alias_names).to_h
65
+ # %%% If with Rails 3.1 and older you get "NoMethodError: undefined method `eq' for nil:NilClass"
66
+ # when trying to call relation.arel, then somewhere along the line while navigating a has_many
67
+ # relationship it can't find the proper foreign key.
68
+ core = relation.arel.ast.cores.first
69
+ # Accommodate AR < 3.2
70
+ arel_alias_names = if core.froms.is_a?(Arel::Table)
71
+ # All recent versions of AR have #source which brings up an Arel::Nodes::JoinSource
72
+ ::DutyFree::Util._recurse_arel(core.source)
73
+ else
74
+ # With AR < 3.2, "froms" brings up the top node, an Arel::Nodes::InnerJoin
75
+ ::DutyFree::Util._recurse_arel(core.froms)
76
+ end
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
90
+ relation = (order_by.empty? ? relation : relation.order(order_by.map { |o| "#{mapping[o.first]}.#{o.last}" }))
91
+ # puts mapping.inspect
92
+ # puts relation.dup.select(template_cols.map { |x| x.to_s(mapping) }).to_sql
93
+
94
+ # Allow customisation of query before running it
95
+ relation = yield(relation, mapping) if block_given?
62
96
 
63
- relation.select(template_cols.map { |x| x.to_s(mapping) }).each do |result|
97
+ relation&.select(template_cols.map { |x| x.to_s(mapping) })&.each do |result|
64
98
  rows << ::DutyFree::Extensions._template_columns(self, import_template).map do |col|
65
99
  value = result.send(col)
66
100
  case value
@@ -77,372 +111,88 @@ module DutyFree
77
111
  rows
78
112
  end
79
113
 
80
- # With an array of incoming data, the first row having column names, perform the import
81
114
  def df_import(data, import_template = nil)
82
- self.instance_variable_set(:@defined_uniques, nil)
83
- self.instance_variable_set(:@valid_uniques, nil)
84
-
85
- import_template ||= if constants.include?(:IMPORT_TEMPLATE)
86
- self::IMPORT_TEMPLATE
87
- else
88
- suggest_template(0, false, false)
89
- end
90
- # puts "Chose #{import_template}"
91
- inserts = []
92
- updates = []
93
- counts = Hash.new { |h, k| h[k] = [] }
94
- errors = []
95
-
96
- is_first = true
97
- uniques = nil
98
- cols = nil
99
- starred = []
100
- partials = []
101
- all = import_template[:all]
102
- keepers = {}
103
- valid_unique = nil
104
- existing = {}
105
- devise_class = ''
106
- ret = nil
107
-
108
- reference_models = if Object.const_defined?('Apartment')
109
- Apartment.excluded_models
110
- else
111
- []
112
- end
113
-
114
- if Object.const_defined?('Devise')
115
- Object.const_get('Devise') # If this fails, devise_class will remain a blank string.
116
- devise_class = Devise.mappings.values.first.class_name
117
- reference_models -= [devise_class]
118
- else
119
- devise_class = ''
120
- end
121
-
122
- # Did they give us a filename?
123
- if data.is_a?(String)
124
- data = if data.length <= 4096 && data.split('\n').length == 1
125
- File.open(data)
126
- else
127
- # Hope that other multi-line strings might be CSV data
128
- CSV.new(data)
129
- end
130
- end
131
- # Or perhaps us a file?
132
- if data.is_a?(File)
133
- # Use the "roo" gem if it's available
134
- data = if Object.const_defined?('Roo::Spreadsheet', { csv_options: { encoding: 'bom|utf-8' } })
135
- Roo::Spreadsheet.open(data)
136
- else
137
- # Otherwise generic CSV parsing
138
- require 'csv' unless Object.const_defined?('CSV')
139
- CSV.open(data)
140
- end
141
- end
142
-
143
- # Will show as just one transaction when using auditing solutions such as PaperTrail
144
- ActiveRecord::Base.transaction do
145
- # Check to see if they want to do anything before the whole import
146
- if before_import ||= (import_template[:before_import]) # || some generic before_import)
147
- before_import.call(data)
148
- end
149
- col_list = nil
150
- data.each_with_index do |row, row_num|
151
- row_errors = {}
152
- if is_first # Anticipate that first row has column names
153
- uniques = import_template[:uniques]
154
-
155
- # Look for UTF-8 BOM in very first cell
156
- row[0] = row[0][3..-1] if row[0].start_with?([239, 187, 191].pack('U*'))
157
- # How about a first character of FEFF or FFFE to support UTF-16 BOMs?
158
- # FE FF big-endian (standard)
159
- # FF FE little-endian
160
- row[0] = row[0][1..-1] if [65_279, 65_534].include?(row[0][0].ord)
161
- cols = row.map { |col| (col || '').strip }
162
-
163
- # Unique column combinations can be called out explicitly in models using uniques: {...}, or to just
164
- # define one column at a time simply mark with an asterisk.
165
- # Track and clean up stars
166
- starred = cols.select do |col|
167
- if col[0] == '*'
168
- col.slice!(0)
169
- col.strip!
170
- end
171
- end
172
- partials = cols.select do |col|
173
- if col[0] == '~'
174
- col.slice!(0)
175
- col.strip!
176
- end
177
- end
178
- # %%% Will the uniques saved into @defined_uniques here just get redefined later
179
- # after the next line, the map! with clean to change out the alias names? So we can't yet set
180
- # col_list?
181
- defined_uniques(uniques, cols, cols.join('|'), starred)
182
- cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) } # %%%
183
- # Make sure that at least half of them match what we know as being good column names
184
- template_column_objects = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template).first
185
- cols.each_with_index do |col, idx|
186
- # prefixes = col_detail.pre_prefix + (col_detail.prefix.blank? ? [] : [col_detail.prefix])
187
- keepers[idx] = template_column_objects.find { |col_obj| col_obj.titleize == col }
188
- # puts "Could not find a match for column #{idx + 1}, #{col}" if keepers[idx].nil?
189
- end
190
- if keepers.length < (cols.length / 2) - 1
191
- raise ::DutyFree::LessThanHalfAreMatchingColumnsError, I18n.t('import.altered_import_template_coumns')
192
- end
115
+ ::DutyFree::Extensions.import(self, data, import_template)
116
+ end
193
117
 
194
- # Returns just the first valid unique lookup set if there are multiple
195
- valid_unique = find_existing(uniques, cols, starred, import_template, keepers, false)
196
- # Make a lookup from unique values to specific IDs
197
- existing = pluck(*([:id] + valid_unique.keys)).each_with_object(existing) do |v, s|
198
- s[v[1..-1].map(&:to_s)] = v.first
199
- s
200
- end
201
- is_first = false
202
- else # Normal row of data
203
- is_insert = false
204
- is_do_save = true
205
- existing_unique = valid_unique.inject([]) do |s, v|
206
- s << if v.last.is_a?(Array)
207
- v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck(MAX_ID).first.to_s
208
- else
209
- binding.pry if v.last.nil?
210
- row[v.last].to_s
211
- end
212
- end
213
- # Check to see if they want to preprocess anything
214
- if @before_process ||= import_template[:before_process]
215
- existing_unique = @before_process.call(valid_unique, existing_unique)
216
- end
217
- obj = if existing.include?(existing_unique)
218
- find(existing[existing_unique])
219
- else
220
- is_insert = true
221
- new
222
- end
223
- sub_obj = nil
224
- is_has_one = false
225
- has_ones = []
226
- polymorphics = []
227
- sub_objects = {}
228
- this_path = nil
229
- keepers.each do |key, v|
230
- klass = nil
231
- next if v.nil?
232
-
233
- # Not the same as the last path?
234
- if this_path && v.path != this_path.split(',').map(&:to_sym) && !is_has_one
235
- # puts sub_obj.class.name
236
- if respond_to?(:around_import_save)
237
- # Send them the sub_obj even if it might be invalid so they can choose
238
- # to make it valid if they wish.
239
- # binding.pry
240
- around_import_save(sub_obj) do |modded_obj = nil|
241
- modded_obj = (modded_obj || sub_obj)
242
- modded_obj.save if sub_obj&.valid?
243
- end
244
- elsif sub_obj&.valid?
245
- # binding.pry if sub_obj.is_a?(Employee) && sub_obj.first_name == 'Andrew'
246
- sub_obj.save
247
- end
248
- end
249
- sub_obj = obj
250
- this_path = +''
251
- v.path.each_with_index do |path_part, idx|
252
- this_path << (this_path.blank? ? path_part.to_s : ",#{path_part}")
253
- unless (sub_next = sub_objects[this_path])
254
- # Check if we're hitting platform data / a lookup thing
255
- assoc = v.prefix_assocs[idx]
256
- # belongs_to some lookup (reference) data
257
- if assoc && reference_models.include?(assoc.class_name)
258
- lookup_match = assoc.klass.find_by(v.name => row[key])
259
- # Do a partial match if this column allows for it
260
- # and we only find one matching result.
261
- if lookup_match.nil? && partials.include?(v.titleize)
262
- lookup_match ||= assoc.klass.where("#{v.name} LIKE '#{row[key]}%'")
263
- lookup_match = (lookup_match.length == 1 ? lookup_match.first : nil)
264
- end
265
- sub_obj.send("#{path_part}=", lookup_match) unless lookup_match.nil?
266
- # Reference data from the platform level means we stop here
267
- sub_obj = nil
268
- break
269
- end
270
- # This works for belongs_to or has_one. has_many gets sorted below.
271
- # Get existing related object, or create a new one
272
- if (sub_next = sub_obj.send(path_part)).nil?
273
- is_has_one = assoc.is_a?(ActiveRecord::Reflection::HasOneReflection)
274
- klass = Object.const_get(assoc&.class_name)
275
- sub_next = if is_has_one
276
- has_ones << v.path
277
- klass.new
278
- else
279
- # Try to find a unique item if one is referenced
280
- sub_bt = nil
281
- begin
282
- # Goofs up if trim_prefix isn't the same name as the class, or if it's
283
- # a self-join? (like when trim_prefix == 'Reports To')
284
- # %%% Need to test this more when the self-join is more than one hop away,
285
- # such as importing orders and having employees come along :)
286
- # if sub_obj.class == klass
287
- # trim_prefix = ''
288
- # # binding.pry
289
- # end
290
- # %%% Maybe instead of passing in "klass" we can give the belongs_to association and build through that instead,
291
- # allowing us to nix the klass.new(criteria) line below.
292
- trim_prefix = v.titleize[0..-(v.name.length + 2)]
293
- trim_prefix << ' ' unless trim_prefix.blank?
294
- if klass == sub_obj.class # Self-referencing thing pointing to us?
295
- # %%% This should be more general than just for self-referencing things.
296
- sub_cols = cols.map { |c| c.start_with?(trim_prefix) ? c[trim_prefix.length..-1] : nil }
297
- # assoc
298
- sub_bt, criteria = klass.find_existing(uniques, sub_cols, starred, import_template, keepers, nil, row, klass, all, '')
299
- else
300
- sub_bt, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, nil, row, klass, all, trim_prefix)
301
- end
302
- rescue ::DutyFree::NoUniqueColumnError
303
- sub_unique = nil
304
- end
305
- # Self-referencing shouldn't build a new one if it couldn't find one
306
- # %%% Can criteria really ever be nil anymore?
307
- unless klass == sub_obj.class && criteria.empty?
308
- sub_bt ||= klass.new(criteria || {})
309
- end
310
- sub_obj.send("#{path_part}=", sub_bt)
311
- sub_bt
312
- end
313
- end
314
- # Look for possible missing polymorphic detail
315
- if assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) &&
316
- (delegate = assoc.send(:delegate_reflection)&.active_record&.reflect_on_association(assoc.source_reflection_name)) &&
317
- delegate.options[:polymorphic]
318
- polymorphics << { parent: sub_next, child: sub_obj, type_col: delegate.foreign_type, id_col: delegate.foreign_key.to_s }
319
- end
320
- # From a has_many?
321
- if sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
322
- # Try to find a unique item if one is referenced
323
- # %%% There is possibility that when bringing in related classes using a nil
324
- # in IMPORT_TEMPLATE[:all] that this will break. Need to test deeply nested things.
325
- start = (v.pre_prefix.blank? ? 0 : v.pre_prefix.length)
326
- trim_prefix = v.titleize[start..-(v.name.length + 2)]
327
- trim_prefix << ' ' unless trim_prefix.blank?
328
- klass = sub_next.klass
329
- # binding.pry if klass.name == 'OrderDetail'
330
-
331
- # assoc.inverse_of is the belongs_to side of the has_many train we came in here on.
332
- sub_hm, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
333
-
334
- # If still not found then create a new related object using this has_many collection
335
- # (criteria.empty? ? nil : sub_next.new(criteria))
336
- sub_next = sub_hm || sub_next.new(criteria)
337
- end
338
- unless sub_next.nil?
339
- # if sub_next.class.name == devise_class && # only for Devise users
340
- # sub_next.email =~ Devise.email_regexp
341
- # if existing.include?([sub_next.email])
342
- # User already exists
343
- # else
344
- # sub_next.invite!
345
- # end
346
- # end
347
- sub_objects[this_path] = sub_next if this_path.present?
348
- end
349
- end
350
- # binding.pry if sub_obj.reports_to
351
- sub_obj = sub_next #if sub_next
352
- end
353
- # binding.pry if sub_obj.nil?
354
- next if sub_obj.nil?
118
+ private
355
119
 
356
- sym = "#{v.name}=".to_sym
357
- next unless sub_obj.respond_to?(sym)
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
358
132
 
359
- col_type = (sub_class = sub_obj.class).columns_hash[v.name.to_s]&.type
360
- if col_type.nil? && (virtual_columns = import_template[:virtual_columns]) &&
361
- (virtual_columns = virtual_columns[this_path] || virtual_columns)
362
- col_type = virtual_columns[v.name]
363
- end
364
- if col_type == :boolean
365
- if row[key].nil?
366
- # Do nothing when it's nil
367
- elsif %w[yes y].include?(row[key]&.downcase) # Used to cover 'true', 't', 'on'
368
- row[key] = true
369
- elsif %w[no n].include?(row[key]&.downcase) # Used to cover 'false', 'f', 'off'
370
- row[key] = false
371
- else
372
- row_errors[v.name] ||= []
373
- row_errors[v.name] << "Boolean value \"#{row[key]}\" in column #{key + 1} not recognized"
374
- end
375
- end
376
- # binding.pry if v.name.to_s == 'first_name' && sub_obj.first_name == 'Nancy'
377
- sub_obj.send(sym, row[key])
378
- # else
379
- # puts " #{sub_class.name} doesn't respond to #{sym}"
380
- end
381
- # Try to save a final sub-object if one exists
382
- sub_obj.save if sub_obj && this_path && !is_has_one && sub_obj.valid?
383
-
384
- # Wire up has_one associations
385
- has_ones.each do |hasone|
386
- parent = sub_objects[hasone[0..-2].map(&:to_s).join(',')] || obj
387
- hasone_object = sub_objects[hasone.map(&:to_s).join(',')]
388
- parent.send("#{hasone[-1]}=", hasone_object) if parent.new_record? || hasone_object.valid?
133
+ key << upn
134
+ value << val
389
135
  end
390
-
391
- # Reinstate any missing polymorphic _type and _id values
392
- polymorphics.each do |poly|
393
- if !poly[:parent].new_record? || poly[:parent].save
394
- poly[:child].send("#{poly[:type_col]}=".to_sym, poly[:parent].class.name)
395
- poly[:child].send("#{poly[:id_col]}=".to_sym, poly[:parent].id)
396
- end
136
+ unless key.empty?
137
+ s[key] = value
138
+ utilised[key] = nil
397
139
  end
398
-
399
- # Give a window of opportunity to tweak user objects controlled by Devise
400
- obj_class = obj.class
401
- is_do_save = if obj_class.respond_to?(:before_devise_save) && obj_class.name == devise_class
402
- obj_class.before_devise_save(obj, existing)
403
- else
404
- true
405
- end
406
-
407
- if obj.valid?
408
- obj.save if is_do_save
409
- # Snag back any changes to the unique columns. (For instance, Devise downcases email addresses.)
410
- existing_unique = valid_unique.keys.inject([]) { |s, v| s << obj.send(v).to_s }
411
- # Update the duplicate counts and inserted / updated results
412
- counts[existing_unique] << row_num
413
- (is_insert ? inserts : updates) << { row_num => existing_unique } if is_do_save
414
- # Track this new object so we can properly sense any duplicates later
415
- existing[existing_unique] = obj.id
416
- else
417
- 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
418
146
  end
419
- errors << { row_num => row_errors } unless row_errors.empty?
420
147
  end
421
- end
422
- duplicates = counts.each_with_object([]) do |v, s|
423
- s += v.last[1..-1].map { |line_num| { line_num => v.first } } if v.last.count > 1
424
148
  s
425
149
  end
426
- # Check to see if they want to do anything after the import
427
- ret = { inserted: inserts, updated: updates, duplicates: duplicates, errors: errors }
428
- if @after_import ||= (import_template[:after_import]) # || some generic after_import)
429
- ret = ret2 if (ret2 = @after_import.call(ret)).is_a?(Hash)
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}"
430
153
  end
154
+ @defined_uniques[col_list] = defined_uniq
431
155
  end
432
- ret
156
+ defined_uniq
433
157
  end
434
158
 
435
159
  # For use with importing, based on the provided column list calculate all valid combinations
436
160
  # of unique columns. If there is no valid combination, throws an error.
437
- # Returns an object found by this means.
438
- def find_existing(uniques, cols, starred, import_template, keepers, train_we_came_in_here_on, row = nil, obj = nil, all = nil, trim_prefix = '')
439
- col_name_offset = trim_prefix.length
440
- @valid_uniques ||= {} # Fancy memoisation
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)
165
+ unless trim_prefix.blank?
166
+ cols = cols.map { |c| c.start_with?(trim_prefix) ? c[trim_prefix.length..-1] : nil }
167
+ starred = starred.each_with_object([]) do |v, s|
168
+ s << v[trim_prefix.length..-1] if v.start_with?(trim_prefix)
169
+ s
170
+ end
171
+ end
441
172
  col_list = cols.join('|')
442
- unless (vus = @valid_uniques[col_list])
443
- # Find all unique combinations that are available based on incoming columns, and
444
- # pair them up with column number mappings.
445
- template_column_objects = ::DutyFree::Extensions._recurse_def(self, all || import_template[:all], import_template).first
173
+
174
+ # First add in foreign key stuff we can find from belongs_to associations (other than the
175
+ # one we might have arrived here upon).
176
+ criteria = {} # Enough detail to find or build a new object
177
+ bt_criteria = {}
178
+ bt_criteria_all_nil = true
179
+ bt_col_indexes = []
180
+ available_bts = []
181
+ only_valid_uniques = (train_we_came_in_here_on == false)
182
+ uniq_lookups = {} # The data, or how to look up the data
183
+
184
+ vus = ((@valid_uniques ||= {})[col_list] ||= {}) # Fancy memoisation
185
+
186
+ # First, get an overall list of AVAILABLE COLUMNS before considering tricky foreign key stuff.
187
+ # ============================================================================================
188
+ # Generate a list of column names matched up with their zero-ordinal column number mapping for
189
+ # all columns from the incoming import data.
190
+ if (is_new_vus = vus.empty?)
191
+ template_column_objects = ::DutyFree::Extensions._recurse_def(
192
+ self,
193
+ template_all || import_template[:all],
194
+ import_template
195
+ ).first
446
196
  available = if trim_prefix.blank?
447
197
  template_column_objects.select { |col| col.pre_prefix.blank? && col.prefix.blank? }
448
198
  else
@@ -452,167 +202,693 @@ module DutyFree
452
202
  trim_prefix_snake == "#{this_prefix}_"
453
203
  end
454
204
  end.map { |avail| avail.name.to_s.titleize }
455
- vus = defined_uniques(uniques, cols, nil, starred).select do |k, _v|
456
- is_good = true
457
- k.each do |k_col|
458
- unless k_col.start_with?(trim_prefix) && available.include?(k_col[col_name_offset..-1])
459
- is_good = false
460
- break
461
- end
462
- end
463
- is_good
464
- end
465
- @valid_uniques[col_list] = vus
466
- end
467
-
468
- # Make sure they have at least one unique combination to take cues from
469
- ret = {}
470
- unless vus.empty? # raise NoUniqueColumnError.new(I18n.t('import.no_unique_column_error'))
471
- # Convert the first entry to a simplified hash, such as:
472
- # {[:investigator_institutions_name, :investigator_institutions_email] => [8, 9], ...}
473
- # to {:name => 8, :email => 9}
474
- key, val = vus.first
475
- key.each_with_index do |k, idx|
476
- ret[k[col_name_offset..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
477
- end
478
205
  end
479
206
 
480
- # %%% If uniqueness is based on something else hanging out on a belongs_to then we're pretty hosed.
481
- # (Case in point, importing Order with related Order Detail and Product, and then Product needs to
482
- # be found or built first before OrderDetail.)
483
- # Might have to do a deferred save kind of thing, and also make sure the product stuff came first
484
- # before the other stuff
207
+ # Process FOREIGN KEY stuff by going through each belongs_to in this model.
208
+ # =========================================================================
209
+ # This list of all valid uniques will help to filter which foreign keys are kept, and also
210
+ # get further filtered later to arrive upon a final set of valid uniques. (Often but not
211
+ # necessarily a specific valid unique as perhaps with a list of users you want to update some
212
+ # folks based on having their email as a unique identifier, and other folks by having a
213
+ # combination of their name and street address as unique, and use both of those possible
214
+ # unique variations to update phone numbers, and do that all as a part of one import.)
215
+ all_vus = _defined_uniques(uniques, cols, col_list, starred, trim_prefix)
485
216
 
486
- # Find by all corresponding columns
217
+ # %%% Ultimately may consider making this recursive
218
+ reflect_on_all_associations.each do |sn_bt|
219
+ next unless sn_bt.belongs_to? && (!train_we_came_in_here_on || sn_bt != train_we_came_in_here_on)
487
220
 
488
- # Add in any foreign key stuff we can find from other belongs_to associations
489
- # %%% This is starting to look like the other BelongsToAssociation code above around line
490
- # 697, so it really needs to be turned into something recursive instead of this two-layer
491
- # thick thing at best.
221
+ # # %%% Make sure there's a starred column we know about from this one
222
+ # uniq_lookups[sn_bt.foreign_key] = nil if only_valid_uniques
492
223
 
493
- # First check the belongs_tos
494
- criteria = {}
495
- bt_criteria = {}
496
- bt_criteria_all_nil = true
497
- bt_col_indexes = []
498
- only_valid_uniques = (train_we_came_in_here_on == false)
499
- bts = reflect_on_all_associations.each_with_object([]) do |sn_assoc, s|
500
- if sn_assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection) &&
501
- (!train_we_came_in_here_on || sn_assoc != train_we_came_in_here_on) # &&
502
- # sn_assoc.klass != self # Omit stuff pointing to us (like self-referencing stuff)
503
- # %%% Make sure there's a starred column we know about from this one
504
- ret[sn_assoc.foreign_key] = nil if only_valid_uniques
505
- s << sn_assoc
506
- end
507
- s
508
- end
509
- bts.each do |sn_bt|
510
224
  # This search prefix becomes something like "Order Details Product "
511
- # binding.pry
512
225
  cols.each_with_index do |bt_col, idx|
513
226
  next if bt_col_indexes.include?(idx) ||
514
- !bt_col&.start_with?(trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} ")
227
+ !bt_col&.start_with?(bt_prefix = (trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} "))
515
228
 
229
+ available_bts << bt_col
516
230
  fk_id = if row
517
- # Max ID so if there are multiple, only the most recent one is picked.
231
+ # Max ID so if there are multiple matches, only the most recent one is picked.
518
232
  # %%% Need to stack these up in case there are multiple
519
233
  # (like first_name, last_name on a referenced employee)
520
- # binding.pry
521
234
  sn_bt.klass.where(keepers[idx].name => row[idx]).limit(1).pluck(MAX_ID).first
522
235
  else
236
+ # elsif is_new_vus
237
+ # # Add to our criteria if this belongs_to is required
238
+ # bt_req_by_default = sn_bt.klass.respond_to?(:belongs_to_required_by_default) &&
239
+ # sn_bt.klass.belongs_to_required_by_default
240
+ # unless !vus.values.first&.include?(idx) &&
241
+ # (sn_bt.options[:optional] || (sn_bt.options[:required] == false) || !bt_req_by_default)
242
+ # # # Add this fk to the criteria
243
+ # # criteria[fk_name] = fk_id
244
+
245
+ # ref = [keepers[idx].name, idx]
246
+ # # bt_criteria[(fk_name = sn_bt.foreign_key)] ||= [sn_bt.klass, []]
247
+ # # bt_criteria[fk_name].last << ref
248
+ # # bt_criteria[bt_col] = [sn_bt.klass, ref]
249
+
250
+ # # Maybe this is the most useful
251
+ # # First array is friendly column names, second is references
252
+ # foreign_uniques = (bt_criteria[sn_bt.name] ||= [sn_bt.klass, [], []])
253
+ # foreign_uniques[1] << ref
254
+ # foreign_uniques[2] << bt_col
255
+ # vus[bt_col] = foreign_uniques # And we can look up this growing set from any foreign column
523
256
  [sn_bt.klass, keepers[idx].name, idx]
524
257
  end
525
258
  if fk_id
526
259
  bt_col_indexes << idx
527
260
  bt_criteria_all_nil = false
528
261
  end
262
+ # If we're processing a row then this list of foreign key column name entries, named such as
263
+ # "order_id" or "product_id" instead of column-specific stuff like "Order Date" and "Product Name",
264
+ # is kept until the last and then gets merged on top of the other criteria before being returned.
529
265
  bt_criteria[(fk_name = sn_bt.foreign_key)] = fk_id
530
- # Add to our criteria if this belongs_to is required
531
- # %%% Rails older than 5.0 handles this stuff differently!
532
- unless sn_bt.options[:optional] || !sn_bt.klass.belongs_to_required_by_default
533
- criteria[fk_name] = fk_id
534
- else # Should not have this fk as a requirement
535
- ret.delete(fk_name) if only_valid_uniques && ret.include?(fk_name)
266
+
267
+ # Check to see if belongs_tos are generally required on this specific table
268
+ bt_req_by_default = sn_bt.klass.respond_to?(:belongs_to_required_by_default) &&
269
+ sn_bt.klass.belongs_to_required_by_default
270
+
271
+ # Add to our CRITERIA just the belongs_to things that check out.
272
+ # ==============================================================
273
+ # The first check, "all_vus.keys.first.none? { |k| k.start_with?(bt_prefix) }"
274
+ # is to see if one of the columns we're working with from the unique that we've chosen
275
+ # comes from the table referenced by this belongs_to (sn_bt).
276
+ #
277
+ # The second check on the :optional option and bt_req_by_default comes directly from
278
+ # how Rails 5 and later checks to see if a specific belongs_to is marked optional
279
+ # (or required), and without having that indication will fall back on checking the model
280
+ # itself to see if it requires belongs_tos by default.
281
+ next if all_vus.keys.first.none? { |k| k.start_with?(bt_prefix) } &&
282
+ (sn_bt.options[:optional] || !bt_req_by_default)
283
+
284
+ # Add to the criteria
285
+ criteria[fk_name] = fk_id
286
+ end
287
+ end
288
+
289
+ # Now circle back find a final list of VALID UNIQUES by re-assessing the list of all valid uniques
290
+ # in relation to the available belongs_tos found in the last foreign key step.
291
+ if is_new_vus
292
+ available += available_bts
293
+ all_vus.each do |k, v|
294
+ combined_k = []
295
+ combined_v = []
296
+ k.each_with_index do |key, idx|
297
+ if available.include?(key)
298
+ combined_k << key
299
+ combined_v << v[idx]
300
+ end
536
301
  end
302
+ vus[combined_k] = combined_v unless combined_k.empty?
537
303
  end
538
304
  end
539
305
 
306
+ # uniq_lookups = vus.inject({}) do |s, v|
307
+ # return s if available_bts.include?(v.first) # These will be provided in criteria, and not uniq_lookups
308
+
309
+ # # uniq_lookups[k[trim_prefix.length..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
310
+ # s[v.first.downcase.tr(' ', '_').to_sym] = v.last
311
+ # s
312
+ # end
313
+
540
314
  new_criteria_all_nil = bt_criteria_all_nil
541
- if train_we_came_in_here_on != false
542
- criteria = ret.each_with_object({}) do |v, s|
543
- next if bt_col_indexes.include?(v.last)
544
315
 
545
- new_criteria_all_nil = false if (s[v.first.to_sym] = row[v.last])
546
- s
316
+ # Make sure they have at least one unique combination to take cues from
317
+ unless vus.empty? # raise NoUniqueColumnError.new(I18n.t('import.no_unique_column_error'))
318
+ # Convert the first entry to a simplified hash, such as:
319
+ # {[:investigator_institutions_name, :investigator_institutions_email] => [8, 9], ...}
320
+ # to {:name => 8, :email => 9}
321
+ key, val = vus.first # Utilise the first identified set of valid uniques
322
+ key.each_with_index do |k, idx|
323
+ next if available_bts.include?(k) # These will be provided in criteria, and not uniq_lookups
324
+
325
+ # uniq_lookups[k[trim_prefix.length..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
326
+ k_sym = k.downcase.tr(' ', '_').to_sym
327
+ v = val[idx]
328
+ uniq_lookups[k_sym] = v # The column number in which to find the data
329
+
330
+ next if only_valid_uniques || bt_col_indexes.include?(v)
331
+
332
+ # Find by all corresponding columns
333
+ if (row_value = row[v])
334
+ new_criteria_all_nil = false
335
+ criteria[k_sym] = row_value # The data, or how to look up the data
336
+ end
547
337
  end
548
338
  end
549
339
 
550
- # Short-circuiting this to only get back the valid_uniques?
551
- return ret.merge(criteria) if only_valid_uniques
552
-
553
- # binding.pry if obj.is_a?(Order)
340
+ return uniq_lookups.merge(criteria) if only_valid_uniques
554
341
  # If there's nothing to match upon then we're out
555
342
  return [nil, {}] if new_criteria_all_nil
556
343
 
557
- # With this criteria, find any matching has_many row we can so we can update it
558
- sub_hm = obj.find do |hm_obj|
559
- is_good = true
560
- criteria.each do |k, v|
561
- if hm_obj.send(k).to_s != v.to_s
562
- is_good = false
563
- break
344
+ # With this criteria, find any matching has_many row we can so we can update it.
345
+ # First try directly looking it up through ActiveRecord.
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
391
+ # If not successful, such as when fields are exposed via helper methods instead of being
392
+ # real columns in the database tables, try this more intensive approach. This is useful
393
+ # if you had full name kind of data coming in on a spreadsheeet, but in the destination
394
+ # table it's broken out to first_name, middle_name, surname. By writing both full_name
395
+ # and full_name= methods, the importer can check to see if this entry is already there,
396
+ # and put a new row in if not, having one incoming name break out to populate three
397
+ # destination columns.
398
+ unless found_object || klass_or_collection.is_a?(Array)
399
+ found_object = klass_or_collection.find do |obj|
400
+ is_good = true
401
+ criteria.each do |k, v|
402
+ if obj.send(k).to_s != v.to_s
403
+ is_good = false
404
+ break
405
+ end
564
406
  end
407
+ is_good
565
408
  end
566
- is_good
567
409
  end
568
- # Try looking it up through ActiveRecord
569
- # %%% Should we perhaps do this first before the more intensive find routine above?
570
- sub_hm = obj.find_by(criteria) if sub_hm.nil?
571
- [sub_hm, criteria.merge(bt_criteria)]
572
- end
410
+ # Standard criteria as well as foreign key column name detail with exact foreign keys
411
+ # that match up to a primary key so that if needed a new related object can be built,
412
+ # complete with all its association detail.
413
+ [found_object, criteria.merge(bt_criteria)]
414
+ end # _find_existing
415
+ end # module ClassMethods
573
416
 
574
- 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
575
474
 
576
- def defined_uniques(uniques, cols = [], col_list = nil, starred = [])
577
- col_list ||= cols.join('|')
578
- @defined_uniques ||= {}
579
- unless (defined_uniq = @defined_uniques[col_list])
580
- utilised = {} # Track columns that have been referenced thusfar
581
- defined_uniq = uniques.each_with_object({}) do |unique, s|
582
- if unique.is_a?(Array)
583
- key = []
584
- value = []
585
- unique.each do |unique_part|
586
- val = cols.index(unique_part_name = unique_part.to_s.titleize)
587
- next if val.nil?
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
588
502
 
589
- key << unique_part_name
590
- 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!
591
537
  end
592
- unless key.empty?
593
- s[key] = value
594
- utilised[key] = nil
538
+ end
539
+ partials = cols.select do |col|
540
+ if col[0] == '~'
541
+ col.slice!(0)
542
+ col.strip!
595
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)
596
583
  else
597
- val = cols.index(unique_part_name = unique.to_s.titleize)
598
- unless val.nil?
599
- s[[unique_part_name]] = [val]
600
- utilised[[unique_part_name]] = 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]
601
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}"
602
752
  end
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?
603
785
  end
604
- (starred - utilised.keys).each { |star| defined_uniq[[star]] = [cols.index(star)] }
605
- @defined_uniques[col_list] = defined_uniq
606
786
  end
607
- 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
608
806
  end
609
- 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)
842
+
843
+ # Called before building any object linked through a has_one or has_many so that foreign key IDs
844
+ # can be added properly to those new objects. Finally at the end also called to save everything.
845
+ def self._save_pending(to_be_saved)
846
+ while (tbs = to_be_saved.pop)
847
+ # puts "Will be calling #{tbs[1].class.name} #{tbs[1]&.id} .#{tbs[2]} #{tbs[3].class.name} #{tbs[3]&.id}"
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
853
+
854
+ ais = (tbs.first.class.respond_to?(:around_import_save) && tbs.first.class.method(:around_import_save)) ||
855
+ (respond_to?(:around_import_save) && method(:around_import_save))
856
+ if ais
857
+ # Send them the sub_obj even if it might be invalid so they can choose
858
+ # to make it valid if they wish.
859
+ ais.call(tbs.first) do |modded_obj = nil|
860
+ modded_obj = (modded_obj || tbs.first)
861
+ modded_obj.save if modded_obj&.valid?
862
+ end
863
+ elsif tbs.first.valid?
864
+ tbs.first.save
865
+ else
866
+ puts "* Unable to save #{tbs.first.inspect}"
867
+ end
868
+
869
+ next if tbs[1].nil? || # From a has_many?
870
+ tbs[0] == tbs[1] || # From a has_one?
871
+ tbs.first.new_record?
872
+
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
884
+ end
885
+ end
610
886
 
611
887
  # The snake-cased column alias names used in the query to export data
612
888
  def self._template_columns(klass, import_template = nil)
613
889
  template_detail_columns = klass.instance_variable_get(:@template_detail_columns)
614
- if (template_import_columns = klass.instance_variable_get(:@template_import_columns)) != import_template
615
- klass.instance_variable_set(:@template_import_columns, template_import_columns = import_template)
890
+ if klass.instance_variable_get(:@template_import_columns) != import_template
891
+ klass.instance_variable_set(:@template_import_columns, import_template)
616
892
  klass.instance_variable_set(:@template_detail_columns, (template_detail_columns = nil))
617
893
  end
618
894
  unless template_detail_columns
@@ -623,39 +899,79 @@ module DutyFree
623
899
  template_detail_columns
624
900
  end
625
901
 
626
- # Recurse and return two arrays -- one with all columns in sequence, and one a hierarchy of
627
- # nested hashes to be used with ActiveRecord's .joins() to facilitate export.
628
- def self._recurse_def(klass, array, import_template, assocs = [], joins = [], pre_prefix = '', prefix = '')
629
- # Confirm we can actually navigate through this association
630
- prefix_assoc = (assocs.last&.klass || klass).reflect_on_association(prefix) if prefix.present?
631
- assocs = assocs.dup << prefix_assoc unless prefix_assoc.nil?
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?('!'))
632
907
  prefixes = ::DutyFree::Util._prefix_join([pre_prefix, prefix])
633
- array = array.inject([]) do |s, col|
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
935
+ end
936
+ end
937
+ cols = cols.inject([]) do |s, col|
634
938
  s + if col.is_a?(Hash)
635
939
  col.inject([]) do |s2, v|
636
- joins << { v.first.to_sym => (joins_array = []) }
637
- s2 += _recurse_def(klass, (v.last.is_a?(Array) ? v.last : [v.last]), import_template, 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
638
947
  end
639
948
  elsif col.nil?
640
949
  if assocs.empty?
641
950
  []
642
951
  else
643
952
  # Bring in from another class
644
- 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
645
956
  # %%% Also bring in uniques and requireds
646
- _recurse_def(klass, assocs.last.klass::IMPORT_TEMPLATE[:all], import_template, 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
647
958
  end
648
959
  else
649
960
  [::DutyFree::Column.new(col, pre_prefix, prefix, assocs, klass, import_template[:as])]
650
961
  end
651
962
  end
652
- [array, joins]
963
+ [cols, joins]
653
964
  end
654
965
  end # module Extensions
966
+ # rubocop:enable Style/CommentedKeyword
655
967
 
656
- class NoUniqueColumnError < ActiveRecord::RecordNotUnique
968
+ # Rails < 4.0 doesn't have ActiveRecord::RecordNotUnique, so use the more generic ActiveRecord::ActiveRecordError instead
969
+ ar_not_unique_error = ActiveRecord.const_defined?('RecordNotUnique') ? ActiveRecord::RecordNotUnique : ActiveRecord::ActiveRecordError
970
+ class NoUniqueColumnError < ar_not_unique_error
657
971
  end
658
972
 
659
- class LessThanHalfAreMatchingColumnsError < ActiveRecord::RecordInvalid
973
+ # Rails < 4.2 doesn't have ActiveRecord::RecordInvalid, so use the more generic ActiveRecord::ActiveRecordError instead
974
+ ar_invalid_error = ActiveRecord.const_defined?('RecordInvalid') ? ActiveRecord::RecordInvalid : ActiveRecord::ActiveRecordError
975
+ class LessThanHalfAreMatchingColumnsError < ar_invalid_error
660
976
  end
661
977
  end