duty_free 1.0.3 → 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: 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