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 +4 -4
- data/lib/duty_free.rb +358 -18
- data/lib/duty_free/column.rb +3 -7
- data/lib/duty_free/extensions.rb +800 -484
- data/lib/duty_free/suggest_template.rb +26 -23
- data/lib/duty_free/util.rb +145 -13
- data/lib/duty_free/version_number.rb +1 -1
- metadata +16 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 714b772aa09eb15409cd7c58d4ccb64f967caf41f2a13e7813e90cc94d7eb48f
|
4
|
+
data.tar.gz: 750dfe36b729979a0356f14f8588568822ae22911c142d294358231504daa78e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d77a228b4e4990dad39882a4e67d1009f3c16114bc0e9b24280e1f8e9437c7fc55a8884eb37e8f6c15898958833c7f79bb7984d721f4dbd1fce7311ca6421d0
|
7
|
+
data.tar.gz: 1d35f3197ab500a6210eecc561190c02a04d87e3d4b2634a9a62695488d4e2ed0825f7240836aa9d32fe8ce01e06be38ddccda1955ca3d3669fd7ad18e8335f2
|
data/lib/duty_free.rb
CHANGED
@@ -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
|
-
#
|
70
|
-
|
71
|
-
#
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
#
|
78
|
-
#
|
79
|
-
|
80
|
-
#
|
81
|
-
|
82
|
-
#
|
83
|
-
#
|
84
|
-
|
85
|
-
|
86
|
-
#
|
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
|
data/lib/duty_free/column.rb
CHANGED
@@ -36,20 +36,16 @@ module DutyFree
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def titleize
|
39
|
-
@
|
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
|
52
|
-
@
|
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('.', '_')
|
data/lib/duty_free/extensions.rb
CHANGED
@@ -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,
|
55
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
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
|
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
|
-
|
83
|
-
|
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
|
-
|
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
|
-
|
357
|
-
|
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
|
-
|
360
|
-
|
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
|
-
|
392
|
-
|
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
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
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
|
-
|
427
|
-
|
428
|
-
|
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
|
-
|
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
|
439
|
-
|
440
|
-
|
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
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
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
|
-
#
|
481
|
-
#
|
482
|
-
#
|
483
|
-
#
|
484
|
-
#
|
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
|
-
#
|
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
|
-
|
489
|
-
|
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
|
-
|
531
|
-
#
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
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
|
-
|
546
|
-
|
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
|
-
|
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
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
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
|
-
#
|
569
|
-
#
|
570
|
-
|
571
|
-
[
|
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
|
-
|
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
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
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
|
-
|
590
|
-
|
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
|
-
|
593
|
-
|
594
|
-
|
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
|
-
|
598
|
-
unless
|
599
|
-
|
600
|
-
|
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
|
-
|
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
|
-
|
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
|
615
|
-
klass.instance_variable_set(:@template_import_columns,
|
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
|
627
|
-
# nested hashes to be used with ActiveRecord's .joins() to facilitate export
|
628
|
-
|
629
|
-
|
630
|
-
|
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
|
-
|
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
|
-
|
637
|
-
|
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
|
-
|
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
|
-
[
|
963
|
+
[cols, joins]
|
653
964
|
end
|
654
965
|
end # module Extensions
|
966
|
+
# rubocop:enable Style/CommentedKeyword
|
655
967
|
|
656
|
-
|
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
|
-
|
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
|