duty_free 1.0.1 → 1.0.6
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 +181 -0
- data/lib/duty_free/column.rb +5 -7
- data/lib/duty_free/extensions.rb +399 -220
- data/lib/duty_free/suggest_template.rb +23 -19
- data/lib/duty_free/util.rb +23 -9
- data/lib/duty_free/version_number.rb +1 -1
- metadata +15 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f9ac922dd807fc18539ff130c1ccf63412e837acbe4eba0127423235c11d0f1e
|
4
|
+
data.tar.gz: e5c27fe4a58718dcbbd4c5ebc1aed2dbb5e7e4b386c977e0ec31269cfad08d48
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54811a68bd34fd5bb881fa19006a7297a8697a3a1575b7060ba7d82e6fcd6d36f545e1d5cfcfc51c756a5b87880e445c415924d2f54aa39471f0b884f0b4c6c4
|
7
|
+
data.tar.gz: 32ccb88e429e64bccb59c7741955b99537c661d6416d6f8c352cb384df728081d5b54079cb2dbd904203fbcf1c60abb1a4a7c70bee9c2764fa1622eaaeb6843f
|
data/lib/duty_free.rb
CHANGED
@@ -1,5 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'active_record/version'
|
4
|
+
|
5
|
+
# ActiveRecord before 4.0 didn't have #version
|
6
|
+
unless ActiveRecord.respond_to?(:version)
|
7
|
+
module ActiveRecord
|
8
|
+
def self.version
|
9
|
+
::Gem::Version.new(ActiveRecord::VERSION::STRING)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# In ActiveSupport older than 5.0, the duplicable? test tries to new up a BigDecimal,
|
15
|
+
# and Ruby 2.6 and later deprecates #new. This removes the warning from BigDecimal.
|
16
|
+
require 'bigdecimal'
|
17
|
+
if ActiveRecord.version < ::Gem::Version.new('5.0') &&
|
18
|
+
::Gem::Version.new(RUBY_VERSION) >= ::Gem::Version.new('2.6')
|
19
|
+
def BigDecimal.new(*args, **kwargs)
|
20
|
+
BigDecimal(*args, **kwargs)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Allow Rails 4.0 and 4.1 to work with newer Ruby (>= 2.4) by avoiding a "stack level too deep" error
|
25
|
+
# when ActiveSupport tries to smarten up Numeric by messing with Fixnum and Bignum at the end of:
|
26
|
+
# activesupport-4.0.13/lib/active_support/core_ext/numeric/conversions.rb
|
27
|
+
if ActiveRecord.version < ::Gem::Version.new('4.2') &&
|
28
|
+
ActiveRecord.version > ::Gem::Version.new('3.2') &&
|
29
|
+
Object.const_defined?('Integer') && Integer.superclass.name == 'Numeric'
|
30
|
+
class OurFixnum < Integer; end
|
31
|
+
Numeric.const_set('Fixnum', OurFixnum)
|
32
|
+
class OurBignum < Integer; end
|
33
|
+
Numeric.const_set('Bignum', OurBignum)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Allow Rails < 3.2 to run with newer versions of Psych gem
|
37
|
+
if BigDecimal.respond_to?(:yaml_tag) && !BigDecimal.respond_to?(:yaml_as)
|
38
|
+
class BigDecimal
|
39
|
+
class <<self
|
40
|
+
alias yaml_as yaml_tag
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
3
45
|
require 'active_record'
|
4
46
|
|
5
47
|
require 'duty_free/config'
|
@@ -62,7 +104,146 @@ module DutyFree
|
|
62
104
|
end
|
63
105
|
end
|
64
106
|
|
107
|
+
# Major compatibility fixes for ActiveRecord < 4.2
|
108
|
+
# ================================================
|
65
109
|
ActiveSupport.on_load(:active_record) do
|
110
|
+
# Rails < 4.0 cannot do #find_by, or do #pluck on multiple columns, so here are the patches:
|
111
|
+
if ActiveRecord.version < ::Gem::Version.new('4.0')
|
112
|
+
module ActiveRecord
|
113
|
+
module Calculations # Normally find_by is in FinderMethods, which older AR doesn't have
|
114
|
+
def find_by(*args)
|
115
|
+
where(*args).limit(1).to_a.first
|
116
|
+
end
|
117
|
+
|
118
|
+
def pluck(*column_names)
|
119
|
+
column_names.map! do |column_name|
|
120
|
+
if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s)
|
121
|
+
"#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
|
122
|
+
else
|
123
|
+
column_name
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Same as: if has_include?(column_names.first)
|
128
|
+
if eager_loading? || (includes_values.present? && (column_names.first || references_eager_loaded_tables?))
|
129
|
+
construct_relation_for_association_calculations.pluck(*column_names)
|
130
|
+
else
|
131
|
+
relation = clone # spawn
|
132
|
+
relation.select_values = column_names
|
133
|
+
result = if klass.connection.class.name.end_with?('::PostgreSQLAdapter')
|
134
|
+
rslt = klass.connection.execute(relation.arel.to_sql)
|
135
|
+
rslt.type_map =
|
136
|
+
@type_map ||= proc do
|
137
|
+
# This aliasing avoids the warning:
|
138
|
+
# "no type cast defined for type "numeric" with oid 1700. Please cast this type
|
139
|
+
# explicitly to TEXT to be safe for future changes."
|
140
|
+
PG::BasicTypeRegistry.alias_type(0, 'numeric', 'text')
|
141
|
+
PG::BasicTypeMapForResults.new(klass.connection.raw_connection)
|
142
|
+
end.call
|
143
|
+
rslt.to_a
|
144
|
+
elsif respond_to?(:bind_values)
|
145
|
+
klass.connection.select_all(relation.arel, nil, bind_values)
|
146
|
+
else
|
147
|
+
klass.connection.select_all(relation.arel.to_sql, nil)
|
148
|
+
end
|
149
|
+
if result.empty?
|
150
|
+
[]
|
151
|
+
else
|
152
|
+
columns = result.first.keys.map do |key|
|
153
|
+
# rubocop:disable Style/SingleLineMethods Naming/MethodParameterName
|
154
|
+
klass.columns_hash.fetch(key) do
|
155
|
+
Class.new { def type_cast(v); v; end }.new
|
156
|
+
end
|
157
|
+
# rubocop:enable Style/SingleLineMethods Naming/MethodParameterName
|
158
|
+
end
|
159
|
+
|
160
|
+
result = result.map do |attributes|
|
161
|
+
values = klass.initialize_attributes(attributes).values
|
162
|
+
|
163
|
+
columns.zip(values).map do |column, value|
|
164
|
+
column.type_cast(value)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
columns.one? ? result.map!(&:first) : result
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
unless Base.is_a?(Calculations)
|
174
|
+
class Base
|
175
|
+
class << self
|
176
|
+
delegate :pluck, :find_by, to: :scoped
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# ActiveRecord < 3.2 doesn't have initialize_attributes, used by .pluck()
|
182
|
+
unless AttributeMethods.const_defined?('Serialization')
|
183
|
+
class Base
|
184
|
+
class << self
|
185
|
+
def initialize_attributes(attributes, options = {}) #:nodoc:
|
186
|
+
serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
|
187
|
+
# super(attributes, options)
|
188
|
+
|
189
|
+
serialized_attributes.each do |key, coder|
|
190
|
+
attributes[key] = Attribute.new(coder, attributes[key], serialized) if attributes.key?(key)
|
191
|
+
end
|
192
|
+
|
193
|
+
attributes
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# This only gets added for ActiveRecord < 3.2
|
200
|
+
module Reflection
|
201
|
+
unless AssociationReflection.instance_methods.include?(:foreign_key)
|
202
|
+
class AssociationReflection < MacroReflection
|
203
|
+
alias foreign_key association_foreign_key
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Rails < 4.2 is not innately compatible with Ruby 2.4 and later, and comes up with:
|
211
|
+
# "TypeError: Cannot visit Integer" unless we patch like this:
|
212
|
+
unless ::Gem::Version.new(RUBY_VERSION) < ::Gem::Version.new('2.4')
|
213
|
+
unless Arel::Visitors::DepthFirst.private_instance_methods.include?(:visit_Integer)
|
214
|
+
module Arel
|
215
|
+
module Visitors
|
216
|
+
class DepthFirst < Visitor
|
217
|
+
alias visit_Integer terminal
|
218
|
+
end
|
219
|
+
|
220
|
+
class Dot < Visitor
|
221
|
+
alias visit_Integer visit_String
|
222
|
+
end
|
223
|
+
|
224
|
+
class ToSql < Visitor
|
225
|
+
private
|
226
|
+
|
227
|
+
# ActiveRecord before v3.2 uses Arel < 3.x, which does not have Arel#literal.
|
228
|
+
unless private_instance_methods.include?(:literal)
|
229
|
+
def literal(obj)
|
230
|
+
obj
|
231
|
+
end
|
232
|
+
end
|
233
|
+
alias visit_Integer literal
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
if ActiveRecord.version < ::Gem::Version.new('5.0')
|
241
|
+
# Avoid pg gem deprecation warning: "You should use PG::Connection, PG::Result, and PG::Error instead"
|
242
|
+
PGconn = PG::Connection
|
243
|
+
PGresult = PG::Result
|
244
|
+
PGError = PG::Error
|
245
|
+
end
|
246
|
+
|
66
247
|
include ::DutyFree::Extensions
|
67
248
|
end
|
68
249
|
|
data/lib/duty_free/column.rb
CHANGED
@@ -28,25 +28,23 @@ module DutyFree
|
|
28
28
|
# sql_col = ::DutyFree::Util._prefix_join([table_name.compact.join('_'), name])
|
29
29
|
|
30
30
|
# Foolproof way, using the AREL mapping:
|
31
|
-
|
31
|
+
this_pre_prefix = pre_prefix.tr('.', '_')
|
32
|
+
this_pre_prefix << '_' unless pre_prefix.blank?
|
33
|
+
sql_col = ::DutyFree::Util._prefix_join([mapping["#{this_pre_prefix}#{prefix}_"], name])
|
32
34
|
sym = to_sym.to_s
|
33
35
|
sql_col == sym ? sql_col : "#{sql_col} AS #{sym}"
|
34
36
|
end
|
35
37
|
|
36
38
|
def titleize
|
37
|
-
@titleized ||=
|
39
|
+
@titleized ||= to_sym.titleize
|
38
40
|
end
|
39
41
|
|
40
|
-
delegate :to_sym, to: :sym_string
|
41
|
-
|
42
42
|
def path
|
43
43
|
@path ||= ::DutyFree::Util._prefix_join([pre_prefix, prefix]).split('.').map(&:to_sym)
|
44
44
|
end
|
45
45
|
|
46
|
-
private
|
47
|
-
|
48
46
|
# The snake-cased column name to be used for building the full list of template_columns
|
49
|
-
def
|
47
|
+
def to_sym
|
50
48
|
@sym_string ||= ::DutyFree::Util._prefix_join(
|
51
49
|
[pre_prefix, prefix, ::DutyFree::Util._clean_name(name, import_template_as)],
|
52
50
|
'_'
|
data/lib/duty_free/extensions.rb
CHANGED
@@ -14,11 +14,13 @@ module DutyFree
|
|
14
14
|
|
15
15
|
# :nodoc:
|
16
16
|
module ClassMethods
|
17
|
+
MAX_ID = Arel.sql('MAX(id)')
|
17
18
|
# def self.extended(model)
|
18
19
|
# end
|
19
20
|
|
20
21
|
# Export at least column header, and optionally include all existing data as well
|
21
|
-
def df_export(is_with_data = true, import_template = nil)
|
22
|
+
def df_export(is_with_data = true, import_template = nil, use_inner_joins = false)
|
23
|
+
use_inner_joins = true unless respond_to?(:left_joins)
|
22
24
|
# In case they are only supplying the columns hash
|
23
25
|
if is_with_data.is_a?(Hash) && !import_template
|
24
26
|
import_template = is_with_data
|
@@ -46,20 +48,31 @@ module DutyFree
|
|
46
48
|
end
|
47
49
|
(is_required ? '* ' : '') + col
|
48
50
|
end
|
49
|
-
rows =
|
51
|
+
rows = [rows]
|
50
52
|
|
51
53
|
if is_with_data
|
54
|
+
order_by = []
|
55
|
+
order_by << ['_', primary_key] if primary_key
|
52
56
|
# Automatically create a JOINs strategy and select list to get back all related rows
|
53
|
-
template_cols, template_joins = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template)
|
54
|
-
relation = left_joins(template_joins)
|
57
|
+
template_cols, template_joins = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template, order_by)
|
58
|
+
relation = use_inner_joins ? joins(template_joins) : left_joins(template_joins)
|
55
59
|
|
56
60
|
# So we can properly create the SELECT list, create a mapping between our
|
57
61
|
# column alias prefixes and the aliases AREL creates.
|
58
|
-
|
59
|
-
|
60
|
-
|
62
|
+
core = relation.arel.ast.cores.first
|
63
|
+
# Accommodate AR < 3.2
|
64
|
+
arel_alias_names = if core.froms.is_a?(Arel::Table)
|
65
|
+
# All recent versions of AR have #source which brings up an Arel::Nodes::JoinSource
|
66
|
+
::DutyFree::Util._recurse_arel(core.source)
|
67
|
+
else
|
68
|
+
# With AR < 3.2, "froms" brings up the top node, an Arel::Nodes::InnerJoin
|
69
|
+
::DutyFree::Util._recurse_arel(core.froms)
|
70
|
+
end
|
71
|
+
our_names = ['_'] + ::DutyFree::Util._recurse_arel(template_joins)
|
61
72
|
mapping = our_names.zip(arel_alias_names).to_h
|
62
|
-
|
73
|
+
relation = (order_by.empty? ? relation : relation.order(order_by.map { |o| "#{mapping[o.first]}.#{o.last}" }))
|
74
|
+
# puts mapping.inspect
|
75
|
+
# puts relation.dup.select(template_cols.map { |x| x.to_s(mapping) }).to_sql
|
63
76
|
relation.select(template_cols.map { |x| x.to_s(mapping) }).each do |result|
|
64
77
|
rows << ::DutyFree::Extensions._template_columns(self, import_template).map do |col|
|
65
78
|
value = result.send(col)
|
@@ -79,15 +92,15 @@ module DutyFree
|
|
79
92
|
|
80
93
|
# With an array of incoming data, the first row having column names, perform the import
|
81
94
|
def df_import(data, import_template = nil)
|
82
|
-
|
83
|
-
|
95
|
+
instance_variable_set(:@defined_uniques, nil)
|
96
|
+
instance_variable_set(:@valid_uniques, nil)
|
84
97
|
|
85
98
|
import_template ||= if constants.include?(:IMPORT_TEMPLATE)
|
86
99
|
self::IMPORT_TEMPLATE
|
87
100
|
else
|
88
101
|
suggest_template(0, false, false)
|
89
102
|
end
|
90
|
-
puts "Chose #{import_template}"
|
103
|
+
# puts "Chose #{import_template}"
|
91
104
|
inserts = []
|
92
105
|
updates = []
|
93
106
|
counts = Hash.new { |h, k| h[k] = [] }
|
@@ -105,6 +118,7 @@ module DutyFree
|
|
105
118
|
devise_class = ''
|
106
119
|
ret = nil
|
107
120
|
|
121
|
+
# Multi-tenancy gem Apartment can be used if there are separate schemas per tenant
|
108
122
|
reference_models = if Object.const_defined?('Apartment')
|
109
123
|
Apartment.excluded_models
|
110
124
|
else
|
@@ -121,10 +135,13 @@ module DutyFree
|
|
121
135
|
|
122
136
|
# Did they give us a filename?
|
123
137
|
if data.is_a?(String)
|
124
|
-
|
138
|
+
# Filenames with full paths can not be longer than 4096 characters, and can not
|
139
|
+
# include newline characters
|
140
|
+
data = if data.length <= 4096 && !data.index('\n')
|
125
141
|
File.open(data)
|
126
142
|
else
|
127
|
-
#
|
143
|
+
# Any multi-line string is likely CSV data
|
144
|
+
# %%% Test to see if TAB characters are present on the first line, instead of commas
|
128
145
|
CSV.new(data)
|
129
146
|
end
|
130
147
|
end
|
@@ -143,10 +160,16 @@ module DutyFree
|
|
143
160
|
# Will show as just one transaction when using auditing solutions such as PaperTrail
|
144
161
|
ActiveRecord::Base.transaction do
|
145
162
|
# Check to see if they want to do anything before the whole import
|
146
|
-
if
|
147
|
-
|
163
|
+
# First if defined in the import_template, then if there is a method in the class,
|
164
|
+
# and finally (not yet implemented) a generic global before_import
|
165
|
+
my_before_import = import_template[:before_import]
|
166
|
+
my_before_import ||= respond_to?(:before_import) && method(:before_import)
|
167
|
+
# my_before_import ||= some generic my_before_import
|
168
|
+
if my_before_import
|
169
|
+
last_arg_idx = my_before_import.parameters.length - 1
|
170
|
+
arguments = [data, import_template][0..last_arg_idx]
|
171
|
+
data = ret if (ret = my_before_import.call(*arguments)).is_a?(Enumerable)
|
148
172
|
end
|
149
|
-
col_list = nil
|
150
173
|
data.each_with_index do |row, row_num|
|
151
174
|
row_errors = {}
|
152
175
|
if is_first # Anticipate that first row has column names
|
@@ -175,11 +198,8 @@ module DutyFree
|
|
175
198
|
col.strip!
|
176
199
|
end
|
177
200
|
end
|
178
|
-
|
179
|
-
# after the next line, the map! with clean to change out the alias names? So we can't yet set
|
180
|
-
# col_list?
|
201
|
+
cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) }
|
181
202
|
defined_uniques(uniques, cols, cols.join('|'), starred)
|
182
|
-
cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) } # %%%
|
183
203
|
# Make sure that at least half of them match what we know as being good column names
|
184
204
|
template_column_objects = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template).first
|
185
205
|
cols.each_with_index do |col, idx|
|
@@ -187,9 +207,7 @@ module DutyFree
|
|
187
207
|
keepers[idx] = template_column_objects.find { |col_obj| col_obj.titleize == col }
|
188
208
|
# puts "Could not find a match for column #{idx + 1}, #{col}" if keepers[idx].nil?
|
189
209
|
end
|
190
|
-
if keepers.length < (cols.length / 2) - 1
|
191
|
-
raise ::DutyFree::LessThanHalfAreMatchingColumnsError, I18n.t('import.altered_import_template_coumns')
|
192
|
-
end
|
210
|
+
raise ::DutyFree::LessThanHalfAreMatchingColumnsError, I18n.t('import.altered_import_template_coumns') if keepers.length < (cols.length / 2) - 1
|
193
211
|
|
194
212
|
# Returns just the first valid unique lookup set if there are multiple
|
195
213
|
valid_unique = find_existing(uniques, cols, starred, import_template, keepers, false)
|
@@ -201,24 +219,22 @@ module DutyFree
|
|
201
219
|
is_first = false
|
202
220
|
else # Normal row of data
|
203
221
|
is_insert = false
|
204
|
-
is_do_save = true
|
205
222
|
existing_unique = valid_unique.inject([]) do |s, v|
|
206
223
|
s << if v.last.is_a?(Array)
|
207
|
-
v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck(
|
224
|
+
v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck(MAX_ID).first.to_s
|
208
225
|
else
|
209
226
|
row[v.last].to_s
|
210
227
|
end
|
211
228
|
end
|
229
|
+
to_be_saved = []
|
212
230
|
# Check to see if they want to preprocess anything
|
213
|
-
if @before_process ||= import_template[:before_process]
|
214
|
-
|
231
|
+
existing_unique = @before_process.call(valid_unique, existing_unique) if @before_process ||= import_template[:before_process]
|
232
|
+
if (criteria = existing[existing_unique])
|
233
|
+
obj = find(criteria)
|
234
|
+
else
|
235
|
+
is_insert = true
|
236
|
+
to_be_saved << [obj = new]
|
215
237
|
end
|
216
|
-
obj = if existing.include?(existing_unique)
|
217
|
-
find(existing[existing_unique])
|
218
|
-
else
|
219
|
-
is_insert = true
|
220
|
-
new
|
221
|
-
end
|
222
238
|
sub_obj = nil
|
223
239
|
is_has_one = false
|
224
240
|
has_ones = []
|
@@ -226,31 +242,15 @@ module DutyFree
|
|
226
242
|
sub_objects = {}
|
227
243
|
this_path = nil
|
228
244
|
keepers.each do |key, v|
|
229
|
-
klass = nil
|
230
245
|
next if v.nil?
|
231
246
|
|
232
|
-
# Not the same as the last path?
|
233
|
-
if this_path && v.path != this_path.split(',').map(&:to_sym) && !is_has_one
|
234
|
-
# puts sub_obj.class.name
|
235
|
-
if respond_to?(:around_import_save)
|
236
|
-
# Send them the sub_obj even if it might be invalid so they can choose
|
237
|
-
# to make it valid if they wish.
|
238
|
-
# binding.pry
|
239
|
-
around_import_save(sub_obj) do |modded_obj = nil|
|
240
|
-
modded_obj = (modded_obj || sub_obj)
|
241
|
-
modded_obj.save if sub_obj&.valid?
|
242
|
-
end
|
243
|
-
elsif sub_obj&.valid?
|
244
|
-
# binding.pry if sub_obj.is_a?(Employee) && sub_obj.first_name == 'Andrew'
|
245
|
-
sub_obj.save
|
246
|
-
end
|
247
|
-
end
|
248
247
|
sub_obj = obj
|
249
248
|
this_path = +''
|
249
|
+
# puts "p: #{v.path}"
|
250
250
|
v.path.each_with_index do |path_part, idx|
|
251
251
|
this_path << (this_path.blank? ? path_part.to_s : ",#{path_part}")
|
252
252
|
unless (sub_next = sub_objects[this_path])
|
253
|
-
# Check if we're hitting
|
253
|
+
# Check if we're hitting reference data / a lookup thing
|
254
254
|
assoc = v.prefix_assocs[idx]
|
255
255
|
# belongs_to some lookup (reference) data
|
256
256
|
if assoc && reference_models.include?(assoc.class_name)
|
@@ -262,74 +262,68 @@ module DutyFree
|
|
262
262
|
lookup_match = (lookup_match.length == 1 ? lookup_match.first : nil)
|
263
263
|
end
|
264
264
|
sub_obj.send("#{path_part}=", lookup_match) unless lookup_match.nil?
|
265
|
-
# Reference data from the
|
265
|
+
# Reference data from the public level means we stop here
|
266
266
|
sub_obj = nil
|
267
267
|
break
|
268
268
|
end
|
269
|
-
# This works for belongs_to or has_one. has_many gets sorted below.
|
270
269
|
# Get existing related object, or create a new one
|
271
|
-
|
272
|
-
|
270
|
+
# This first part works for belongs_to. has_many and has_one get sorted below.
|
271
|
+
if (sub_next = sub_obj.send(path_part)).nil? && assoc.belongs_to?
|
273
272
|
klass = Object.const_get(assoc&.class_name)
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
if klass == sub_obj.class # Self-referencing thing pointing to us?
|
294
|
-
# %%% This should be more general than just for self-referencing things.
|
295
|
-
sub_cols = cols.map { |c| c.start_with?(trim_prefix) ? c[trim_prefix.length..-1] : nil }
|
296
|
-
sub_bt, criteria = klass.find_existing(uniques, sub_cols, starred, import_template, keepers, assoc, row, klass, all, trim_prefix)
|
297
|
-
else
|
298
|
-
sub_bt, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, nil, row, klass, all, trim_prefix)
|
299
|
-
end
|
300
|
-
rescue ::DutyFree::NoUniqueColumnError
|
301
|
-
sub_unique = nil
|
302
|
-
end
|
303
|
-
# binding.pry if sub_obj.is_a?(Employee) && sub_obj.first_name == 'Nancy' &&
|
304
|
-
# sub_bt.is_a?(Employee)
|
305
|
-
# %%% Can criteria really ever be nil anymore?
|
306
|
-
sub_bt ||= klass.new(criteria || {})
|
307
|
-
sub_obj.send("#{path_part}=", sub_bt)
|
308
|
-
sub_bt # unless klass == sub_obj.class # Don't go further if it's self-referencing
|
309
|
-
end
|
310
|
-
end
|
311
|
-
# Look for possible missing polymorphic detail
|
312
|
-
if assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) &&
|
313
|
-
(delegate = assoc.send(:delegate_reflection)&.active_record&.reflect_on_association(assoc.source_reflection_name)) &&
|
314
|
-
delegate.options[:polymorphic]
|
315
|
-
polymorphics << { parent: sub_next, child: sub_obj, type_col: delegate.foreign_type, id_col: delegate.foreign_key.to_s }
|
316
|
-
end
|
317
|
-
# From a has_many?
|
318
|
-
if sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
|
273
|
+
# Try to find a unique item if one is referenced
|
274
|
+
sub_next = nil
|
275
|
+
begin
|
276
|
+
trim_prefix = v.titleize[0..-(v.name.length + 2)]
|
277
|
+
trim_prefix << ' ' unless trim_prefix.blank?
|
278
|
+
sub_next, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, nil, row, klass, all, trim_prefix)
|
279
|
+
rescue ::DutyFree::NoUniqueColumnError
|
280
|
+
end
|
281
|
+
# puts "#{v.path} #{criteria.inspect}"
|
282
|
+
bt_name = "#{path_part}="
|
283
|
+
unless sub_next || (klass == sub_obj.class && criteria.empty?)
|
284
|
+
sub_next = klass.new(criteria || {})
|
285
|
+
to_be_saved << [sub_next, sub_obj, bt_name, sub_next]
|
286
|
+
end
|
287
|
+
sub_obj.send(bt_name, sub_next)
|
288
|
+
# From a has_many or has_one?
|
289
|
+
# Rails 4.0 and later can do: sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
|
290
|
+
elsif [:has_many, :has_one].include?(assoc.macro) && !assoc.options[:through]
|
291
|
+
::DutyFree::Extensions._save_pending(to_be_saved)
|
319
292
|
# Try to find a unique item if one is referenced
|
320
293
|
# %%% There is possibility that when bringing in related classes using a nil
|
321
294
|
# in IMPORT_TEMPLATE[:all] that this will break. Need to test deeply nested things.
|
322
295
|
start = (v.pre_prefix.blank? ? 0 : v.pre_prefix.length)
|
323
296
|
trim_prefix = v.titleize[start..-(v.name.length + 2)]
|
324
297
|
trim_prefix << ' ' unless trim_prefix.blank?
|
325
|
-
klass = sub_next.klass
|
326
|
-
# binding.pry if klass.name == 'OrderDetail'
|
327
|
-
|
328
298
|
# assoc.inverse_of is the belongs_to side of the has_many train we came in here on.
|
329
|
-
sub_hm, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
|
330
|
-
|
299
|
+
sub_hm, criteria = assoc.klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
|
331
300
|
# If still not found then create a new related object using this has_many collection
|
332
|
-
|
301
|
+
# (criteria.empty? ? nil : sub_next.new(criteria))
|
302
|
+
if sub_hm
|
303
|
+
sub_next = sub_hm
|
304
|
+
elsif assoc.macro == :has_one
|
305
|
+
bt_name = "#{assoc.inverse.name}="
|
306
|
+
sub_next = assoc.klass.new(criteria)
|
307
|
+
to_be_saved << [sub_next, sub_obj, bt_name, sub_next]
|
308
|
+
else
|
309
|
+
# Two other methods that are possible to check for here are :conditions and
|
310
|
+
# :sanitized_conditions, which do not exist in Rails 4.0 and later.
|
311
|
+
sub_next = if assoc.respond_to?(:require_association)
|
312
|
+
# With Rails < 4.0, sub_next could come in as an Array or a broken CollectionProxy
|
313
|
+
assoc.klass.new({ fk_from(assoc) => sub_obj.send(sub_obj.class.primary_key) }.merge(criteria))
|
314
|
+
else
|
315
|
+
sub_next.proxy_association.reflection.instance_variable_set(:@foreign_key, fk_from(assoc))
|
316
|
+
sub_next.new(criteria)
|
317
|
+
end
|
318
|
+
to_be_saved << [sub_next]
|
319
|
+
end
|
320
|
+
end
|
321
|
+
# Look for possible missing polymorphic detail
|
322
|
+
# Maybe can test for this via assoc.through_reflection
|
323
|
+
if assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) &&
|
324
|
+
(delegate = assoc.send(:delegate_reflection)&.active_record&.reflect_on_association(assoc.source_reflection_name)) &&
|
325
|
+
delegate.options[:polymorphic]
|
326
|
+
polymorphics << { parent: sub_next, child: sub_obj, type_col: delegate.foreign_type, id_col: delegate.foreign_key.to_s }
|
333
327
|
end
|
334
328
|
unless sub_next.nil?
|
335
329
|
# if sub_next.class.name == devise_class && # only for Devise users
|
@@ -343,14 +337,13 @@ module DutyFree
|
|
343
337
|
sub_objects[this_path] = sub_next if this_path.present?
|
344
338
|
end
|
345
339
|
end
|
346
|
-
sub_obj = sub_next
|
340
|
+
sub_obj = sub_next
|
347
341
|
end
|
348
342
|
next if sub_obj.nil?
|
349
343
|
|
350
|
-
sym = "#{v.name}=".to_sym
|
351
|
-
next unless sub_obj.respond_to?(sym)
|
344
|
+
next unless sub_obj.respond_to?(sym = "#{v.name}=".to_sym)
|
352
345
|
|
353
|
-
col_type =
|
346
|
+
col_type = sub_obj.class.columns_hash[v.name.to_s]&.type
|
354
347
|
if col_type.nil? && (virtual_columns = import_template[:virtual_columns]) &&
|
355
348
|
(virtual_columns = virtual_columns[this_path] || virtual_columns)
|
356
349
|
col_type = virtual_columns[v.name]
|
@@ -358,9 +351,9 @@ module DutyFree
|
|
358
351
|
if col_type == :boolean
|
359
352
|
if row[key].nil?
|
360
353
|
# Do nothing when it's nil
|
361
|
-
elsif %w[yes y].include?(row[key]&.downcase) # Used to cover '
|
354
|
+
elsif %w[true t yes y].include?(row[key]&.strip&.downcase) # Used to cover 'on'
|
362
355
|
row[key] = true
|
363
|
-
elsif %w[no n].include?(row[key]&.downcase) # Used to cover '
|
356
|
+
elsif %w[false f no n].include?(row[key]&.strip&.downcase) # Used to cover 'off'
|
364
357
|
row[key] = false
|
365
358
|
else
|
366
359
|
row_errors[v.name] ||= []
|
@@ -369,17 +362,18 @@ module DutyFree
|
|
369
362
|
end
|
370
363
|
sub_obj.send(sym, row[key])
|
371
364
|
# else
|
372
|
-
# puts " #{
|
365
|
+
# puts " #{sub_obj.class.name} doesn't respond to #{sym}"
|
373
366
|
end
|
367
|
+
::DutyFree::Extensions._save_pending(to_be_saved)
|
374
368
|
# Try to save a final sub-object if one exists
|
375
|
-
sub_obj.save if sub_obj && this_path && !is_has_one && sub_obj.valid?
|
369
|
+
# sub_obj.save if sub_obj && this_path && !is_has_one && sub_obj.valid?
|
376
370
|
|
377
|
-
# Wire up has_one associations
|
378
|
-
has_ones.each do |hasone|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
end
|
371
|
+
# # Wire up has_one associations
|
372
|
+
# has_ones.each do |hasone|
|
373
|
+
# parent = sub_objects[hasone[0..-2].map(&:to_s).join(',')] || obj
|
374
|
+
# hasone_object = sub_objects[hasone.map(&:to_s).join(',')]
|
375
|
+
# parent.send("#{hasone[-1]}=", hasone_object) if parent.new_record? || hasone_object.valid?
|
376
|
+
# end
|
383
377
|
|
384
378
|
# Reinstate any missing polymorphic _type and _id values
|
385
379
|
polymorphics.each do |poly|
|
@@ -416,26 +410,94 @@ module DutyFree
|
|
416
410
|
s += v.last[1..-1].map { |line_num| { line_num => v.first } } if v.last.count > 1
|
417
411
|
s
|
418
412
|
end
|
419
|
-
# Check to see if they want to do anything after the import
|
420
413
|
ret = { inserted: inserts, updated: updates, duplicates: duplicates, errors: errors }
|
421
|
-
|
422
|
-
|
414
|
+
|
415
|
+
# Check to see if they want to do anything after the import
|
416
|
+
# First if defined in the import_template, then if there is a method in the class,
|
417
|
+
# and finally (not yet implemented) a generic global after_import
|
418
|
+
my_after_import = import_template[:after_import]
|
419
|
+
my_after_import ||= respond_to?(:after_import) && method(:after_import)
|
420
|
+
# my_after_import ||= some generic my_after_import
|
421
|
+
if my_after_import
|
422
|
+
last_arg_idx = my_after_import.parameters.length - 1
|
423
|
+
arguments = [ret][0..last_arg_idx]
|
424
|
+
ret = ret2 if (ret2 = my_after_import.call(*arguments)).is_a?(Hash)
|
423
425
|
end
|
424
426
|
end
|
425
427
|
ret
|
426
428
|
end
|
427
429
|
|
430
|
+
def fk_from(assoc)
|
431
|
+
# Try first to trust whatever they've marked as being the foreign_key, and then look
|
432
|
+
# at the inverse's foreign key setting if available. In all cases don't accept
|
433
|
+
# anything that's not backed with a real column in the table.
|
434
|
+
col_names = assoc.klass.column_names
|
435
|
+
if (fk_name = assoc.options[:foreign_key]) && col_names.include?(fk_name.to_s)
|
436
|
+
return fk_name
|
437
|
+
end
|
438
|
+
if (fk_name = assoc.inverse_of&.options&.fetch(:foreign_key) { nil }) &&
|
439
|
+
col_names.include?(fk_name.to_s)
|
440
|
+
return fk_name
|
441
|
+
end
|
442
|
+
if assoc.respond_to?(:foreign_key) && (fk_name = assoc.foreign_key) &&
|
443
|
+
col_names.include?(fk_name.to_s)
|
444
|
+
return fk_name
|
445
|
+
end
|
446
|
+
if (fk_name = assoc.inverse_of&.foreign_key) &&
|
447
|
+
col_names.include?(fk_name.to_s)
|
448
|
+
return fk_name
|
449
|
+
end
|
450
|
+
if (fk_name = assoc.inverse_of&.association_foreign_key) &&
|
451
|
+
col_names.include?(fk_name.to_s)
|
452
|
+
return fk_name
|
453
|
+
end
|
454
|
+
# Don't let this fool you -- we really are in search of the foreign key name here,
|
455
|
+
# and Rails 3.0 and older used some fairly interesting conventions, calling it instead
|
456
|
+
# the "primary_key_name"!
|
457
|
+
if assoc.respond_to?(:primary_key_name) && (fk_name = assoc.primary_key_name) &&
|
458
|
+
col_names.include?(fk_name.to_s)
|
459
|
+
return fk_name
|
460
|
+
end
|
461
|
+
|
462
|
+
puts "* Wow, had no luck at all finding a foreign key for #{assoc.inspect}"
|
463
|
+
end
|
464
|
+
|
428
465
|
# For use with importing, based on the provided column list calculate all valid combinations
|
429
466
|
# of unique columns. If there is no valid combination, throws an error.
|
430
467
|
# Returns an object found by this means.
|
431
|
-
def find_existing(uniques, cols, starred, import_template, keepers, train_we_came_in_here_on,
|
432
|
-
|
433
|
-
|
468
|
+
def find_existing(uniques, cols, starred, import_template, keepers, train_we_came_in_here_on,
|
469
|
+
row = nil, klass_or_collection = nil, template_all = nil, trim_prefix = '')
|
470
|
+
unless trim_prefix.blank?
|
471
|
+
cols = cols.map { |c| c.start_with?(trim_prefix) ? c[trim_prefix.length..-1] : nil }
|
472
|
+
starred = starred.each_with_object([]) do |v, s|
|
473
|
+
s << v[trim_prefix.length..-1] if v.start_with?(trim_prefix)
|
474
|
+
s
|
475
|
+
end
|
476
|
+
end
|
434
477
|
col_list = cols.join('|')
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
478
|
+
|
479
|
+
# First add in foreign key stuff we can find from belongs_to associations (other than the
|
480
|
+
# one we might have arrived here upon).
|
481
|
+
criteria = {} # Enough detail to find or build a new object
|
482
|
+
bt_criteria = {}
|
483
|
+
bt_criteria_all_nil = true
|
484
|
+
bt_col_indexes = []
|
485
|
+
available_bts = []
|
486
|
+
only_valid_uniques = (train_we_came_in_here_on == false)
|
487
|
+
uniq_lookups = {} # The data, or how to look up the data
|
488
|
+
|
489
|
+
vus = ((@valid_uniques ||= {})[col_list] ||= {}) # Fancy memoisation
|
490
|
+
|
491
|
+
# First, get an overall list of AVAILABLE COLUMNS before considering tricky foreign key stuff.
|
492
|
+
# ============================================================================================
|
493
|
+
# Generate a list of column names matched up with their zero-ordinal column number mapping for
|
494
|
+
# all columns from the incoming import data.
|
495
|
+
if (is_new_vus = vus.empty?)
|
496
|
+
template_column_objects = ::DutyFree::Extensions._recurse_def(
|
497
|
+
self,
|
498
|
+
template_all || import_template[:all],
|
499
|
+
import_template
|
500
|
+
).first
|
439
501
|
available = if trim_prefix.blank?
|
440
502
|
template_column_objects.select { |col| col.pre_prefix.blank? && col.prefix.blank? }
|
441
503
|
else
|
@@ -445,113 +507,190 @@ module DutyFree
|
|
445
507
|
trim_prefix_snake == "#{this_prefix}_"
|
446
508
|
end
|
447
509
|
end.map { |avail| avail.name.to_s.titleize }
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
510
|
+
end
|
511
|
+
|
512
|
+
# Process FOREIGN KEY stuff by going through each belongs_to in this model.
|
513
|
+
# =========================================================================
|
514
|
+
# This list of all valid uniques will help to filter which foreign keys are kept, and also
|
515
|
+
# get further filtered later to arrive upon a final set of valid uniques. (Often but not
|
516
|
+
# necessarily a specific valid unique as perhaps with a list of users you want to update some
|
517
|
+
# folks based on having their email as a unique identifier, and other folks by having a
|
518
|
+
# combination of their name and street address as unique, and use both of those possible
|
519
|
+
# unique variations to update phone numbers, and do that all as a part of one import.)
|
520
|
+
all_vus = defined_uniques(uniques, cols, col_list, starred, trim_prefix)
|
521
|
+
|
522
|
+
# %%% Ultimately may consider making this recursive
|
523
|
+
reflect_on_all_associations.each do |sn_bt|
|
524
|
+
next unless sn_bt.belongs_to? && (!train_we_came_in_here_on || sn_bt != train_we_came_in_here_on)
|
525
|
+
|
526
|
+
# # %%% Make sure there's a starred column we know about from this one
|
527
|
+
# uniq_lookups[sn_bt.foreign_key] = nil if only_valid_uniques
|
528
|
+
|
529
|
+
# This search prefix becomes something like "Order Details Product "
|
530
|
+
cols.each_with_index do |bt_col, idx|
|
531
|
+
next if bt_col_indexes.include?(idx) ||
|
532
|
+
!bt_col&.start_with?(bt_prefix = (trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} "))
|
533
|
+
|
534
|
+
available_bts << bt_col
|
535
|
+
fk_id = if row
|
536
|
+
# Max ID so if there are multiple matches, only the most recent one is picked.
|
537
|
+
# %%% Need to stack these up in case there are multiple
|
538
|
+
# (like first_name, last_name on a referenced employee)
|
539
|
+
sn_bt.klass.where(keepers[idx].name => row[idx]).limit(1).pluck(MAX_ID).first
|
540
|
+
else
|
541
|
+
# elsif is_new_vus
|
542
|
+
# # Add to our criteria if this belongs_to is required
|
543
|
+
# bt_req_by_default = sn_bt.klass.respond_to?(:belongs_to_required_by_default) &&
|
544
|
+
# sn_bt.klass.belongs_to_required_by_default
|
545
|
+
# unless !vus.values.first&.include?(idx) &&
|
546
|
+
# (sn_bt.options[:optional] || (sn_bt.options[:required] == false) || !bt_req_by_default)
|
547
|
+
# # # Add this fk to the criteria
|
548
|
+
# # criteria[fk_name] = fk_id
|
549
|
+
|
550
|
+
# ref = [keepers[idx].name, idx]
|
551
|
+
# # bt_criteria[(fk_name = sn_bt.foreign_key)] ||= [sn_bt.klass, []]
|
552
|
+
# # bt_criteria[fk_name].last << ref
|
553
|
+
# # bt_criteria[bt_col] = [sn_bt.klass, ref]
|
554
|
+
|
555
|
+
# # Maybe this is the most useful
|
556
|
+
# # First array is friendly column names, second is references
|
557
|
+
# foreign_uniques = (bt_criteria[sn_bt.name] ||= [sn_bt.klass, [], []])
|
558
|
+
# foreign_uniques[1] << ref
|
559
|
+
# foreign_uniques[2] << bt_col
|
560
|
+
# vus[bt_col] = foreign_uniques # And we can look up this growing set from any foreign column
|
561
|
+
[sn_bt.klass, keepers[idx].name, idx]
|
562
|
+
end
|
563
|
+
if fk_id
|
564
|
+
bt_col_indexes << idx
|
565
|
+
bt_criteria_all_nil = false
|
566
|
+
end
|
567
|
+
# If we're processing a row then this list of foreign key column name entries, named such as
|
568
|
+
# "order_id" or "product_id" instead of column-specific stuff like "Order Date" and "Product Name",
|
569
|
+
# is kept until the last and then gets merged on top of the other criteria before being returned.
|
570
|
+
bt_criteria[(fk_name = sn_bt.foreign_key)] = fk_id
|
571
|
+
|
572
|
+
# Check to see if belongs_tos are generally required on this specific table
|
573
|
+
bt_req_by_default = sn_bt.klass.respond_to?(:belongs_to_required_by_default) &&
|
574
|
+
sn_bt.klass.belongs_to_required_by_default
|
575
|
+
|
576
|
+
# Add to our CRITERIA just the belongs_to things that check out.
|
577
|
+
# ==============================================================
|
578
|
+
# The first check, "all_vus.keys.first.none? { |k| k.start_with?(bt_prefix) }"
|
579
|
+
# is to see if one of the columns we're working with from the unique that we've chosen
|
580
|
+
# comes from the table referenced by this belongs_to (sn_bt).
|
581
|
+
#
|
582
|
+
# The second check on the :optional option and bt_req_by_default comes directly from
|
583
|
+
# how Rails 5 and later checks to see if a specific belongs_to is marked optional
|
584
|
+
# (or required), and without having that indication will fall back on checking the model
|
585
|
+
# itself to see if it requires belongs_tos by default.
|
586
|
+
next if all_vus.keys.first.none? { |k| k.start_with?(bt_prefix) } &&
|
587
|
+
(sn_bt.options[:optional] || !bt_req_by_default)
|
588
|
+
|
589
|
+
# Add to the criteria
|
590
|
+
criteria[fk_name] = fk_id
|
591
|
+
end
|
592
|
+
end
|
593
|
+
|
594
|
+
# Now circle back find a final list of VALID UNIQUES by re-assessing the list of all valid uniques
|
595
|
+
# in relation to the available belongs_tos found in the last foreign key step.
|
596
|
+
if is_new_vus
|
597
|
+
available += available_bts
|
598
|
+
all_vus.each do |k, v|
|
599
|
+
combined_k = []
|
600
|
+
combined_v = []
|
601
|
+
k.each_with_index do |key, idx|
|
602
|
+
if available.include?(key)
|
603
|
+
combined_k << key
|
604
|
+
combined_v << v[idx]
|
454
605
|
end
|
455
606
|
end
|
456
|
-
|
607
|
+
vus[combined_k] = combined_v unless combined_k.empty?
|
457
608
|
end
|
458
|
-
@valid_uniques[col_list] = vus
|
459
609
|
end
|
460
610
|
|
611
|
+
# uniq_lookups = vus.inject({}) do |s, v|
|
612
|
+
# return s if available_bts.include?(v.first) # These will be provided in criteria, and not uniq_lookups
|
613
|
+
|
614
|
+
# # uniq_lookups[k[trim_prefix.length..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
|
615
|
+
# s[v.first.downcase.tr(' ', '_').to_sym] = v.last
|
616
|
+
# s
|
617
|
+
# end
|
618
|
+
|
619
|
+
new_criteria_all_nil = bt_criteria_all_nil
|
620
|
+
|
461
621
|
# Make sure they have at least one unique combination to take cues from
|
462
|
-
ret = {}
|
463
622
|
unless vus.empty? # raise NoUniqueColumnError.new(I18n.t('import.no_unique_column_error'))
|
464
623
|
# Convert the first entry to a simplified hash, such as:
|
465
624
|
# {[:investigator_institutions_name, :investigator_institutions_email] => [8, 9], ...}
|
466
625
|
# to {:name => 8, :email => 9}
|
467
|
-
key, val = vus.first
|
626
|
+
key, val = vus.first # Utilise the first identified set of valid uniques
|
468
627
|
key.each_with_index do |k, idx|
|
469
|
-
|
470
|
-
end
|
471
|
-
end
|
628
|
+
next if available_bts.include?(k) # These will be provided in criteria, and not uniq_lookups
|
472
629
|
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
# before the other stuff
|
478
|
-
|
479
|
-
# Add in any foreign key stuff we can find from other belongs_to associations
|
480
|
-
# %%% This is starting to look like the other BelongsToAssociation code above around line
|
481
|
-
# 697, so it really needs to be turned into something recursive instead of this two-layer
|
482
|
-
# thick thing at best.
|
483
|
-
bts = reflect_on_all_associations.each_with_object([]) do |sn_assoc, s|
|
484
|
-
if sn_assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection) &&
|
485
|
-
(!train_we_came_in_here_on || sn_assoc != train_we_came_in_here_on) &&
|
486
|
-
sn_assoc.klass != self # Omit stuff pointing to us (like self-referencing stuff)
|
487
|
-
# %%% Make sure there's a starred column we know about from this one
|
488
|
-
ret[sn_assoc.foreign_key] = nil if train_we_came_in_here_on == false
|
489
|
-
s << sn_assoc
|
490
|
-
end
|
491
|
-
s
|
492
|
-
end
|
493
|
-
|
494
|
-
# Find by all corresponding columns
|
495
|
-
criteria = {}
|
496
|
-
if train_we_came_in_here_on != false
|
497
|
-
criteria = ret.each_with_object({}) do |v, s|
|
498
|
-
s[v.first.to_sym] = row[v.last]
|
499
|
-
s
|
500
|
-
end
|
501
|
-
end
|
630
|
+
# uniq_lookups[k[trim_prefix.length..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
|
631
|
+
k_sym = k.downcase.tr(' ', '_').to_sym
|
632
|
+
v = val[idx]
|
633
|
+
uniq_lookups[k_sym] = v # The column number in which to find the data
|
502
634
|
|
503
|
-
|
504
|
-
# This search prefix becomes something like "Order Details Product "
|
505
|
-
cols.each_with_index do |bt_col, idx|
|
506
|
-
next unless bt_col.start_with?(trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} ")
|
635
|
+
next if only_valid_uniques || bt_col_indexes.include?(v)
|
507
636
|
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
[sn_bt.klass, keepers[idx].name, idx]
|
514
|
-
end
|
515
|
-
criteria[sn_bt.foreign_key] = fk_id
|
637
|
+
# Find by all corresponding columns
|
638
|
+
if (row_value = row[v])
|
639
|
+
new_criteria_all_nil = false
|
640
|
+
criteria[k_sym] = row_value # The data, or how to look up the data
|
641
|
+
end
|
516
642
|
end
|
517
643
|
end
|
518
644
|
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
645
|
+
return uniq_lookups.merge(criteria) if only_valid_uniques
|
646
|
+
|
647
|
+
# If there's nothing to match upon then we're out
|
648
|
+
return [nil, {}] if new_criteria_all_nil
|
649
|
+
|
650
|
+
# With this criteria, find any matching has_many row we can so we can update it.
|
651
|
+
# First try directly looking it up through ActiveRecord.
|
652
|
+
found_object = klass_or_collection.find_by(criteria)
|
653
|
+
# If not successful, such as when fields are exposed via helper methods instead of being
|
654
|
+
# real columns in the database tables, try this more intensive approach. This is useful
|
655
|
+
# if you had full name kind of data coming in on a spreadsheeet, but in the destination
|
656
|
+
# table it's broken out to first_name, middle_name, surname. By writing both full_name
|
657
|
+
# and full_name= methods, the importer can check to see if this entry is already there,
|
658
|
+
# and put a new row in if not, having one incoming name break out to populate three
|
659
|
+
# destination columns.
|
660
|
+
unless found_object || klass_or_collection.is_a?(Array)
|
661
|
+
found_object = klass_or_collection.find do |obj|
|
662
|
+
is_good = true
|
663
|
+
criteria.each do |k, v|
|
664
|
+
if obj.send(k).to_s != v.to_s
|
665
|
+
is_good = false
|
666
|
+
break
|
667
|
+
end
|
529
668
|
end
|
669
|
+
is_good
|
530
670
|
end
|
531
|
-
is_good
|
532
671
|
end
|
533
|
-
#
|
534
|
-
#
|
535
|
-
|
536
|
-
[
|
672
|
+
# Standard criteria as well as foreign key column name detail with exact foreign keys
|
673
|
+
# that match up to a primary key so that if needed a new related object can be built,
|
674
|
+
# complete with all its association detail.
|
675
|
+
[found_object, criteria.merge(bt_criteria)]
|
537
676
|
end
|
538
677
|
|
539
678
|
private
|
540
679
|
|
541
|
-
def defined_uniques(uniques, cols = [], col_list = nil, starred = [])
|
680
|
+
def defined_uniques(uniques, cols = [], col_list = nil, starred = [], trim_prefix = '')
|
542
681
|
col_list ||= cols.join('|')
|
543
|
-
@defined_uniques ||= {}
|
544
|
-
unless (defined_uniq = @defined_uniques[col_list])
|
682
|
+
unless (defined_uniq = (@defined_uniques ||= {})[col_list])
|
545
683
|
utilised = {} # Track columns that have been referenced thusfar
|
546
684
|
defined_uniq = uniques.each_with_object({}) do |unique, s|
|
547
685
|
if unique.is_a?(Array)
|
548
686
|
key = []
|
549
687
|
value = []
|
550
688
|
unique.each do |unique_part|
|
551
|
-
val =
|
552
|
-
|
689
|
+
val = (unique_part_name = unique_part.to_s.titleize).start_with?(trim_prefix) &&
|
690
|
+
cols.index(upn = unique_part_name[trim_prefix.length..-1])
|
691
|
+
next unless val
|
553
692
|
|
554
|
-
key <<
|
693
|
+
key << upn
|
555
694
|
value << val
|
556
695
|
end
|
557
696
|
unless key.empty?
|
@@ -559,29 +698,60 @@ module DutyFree
|
|
559
698
|
utilised[key] = nil
|
560
699
|
end
|
561
700
|
else
|
562
|
-
val =
|
563
|
-
|
564
|
-
|
565
|
-
|
701
|
+
val = (unique_name = unique.to_s.titleize).start_with?(trim_prefix) &&
|
702
|
+
cols.index(un = unique_name[trim_prefix.length..-1])
|
703
|
+
if val
|
704
|
+
s[[un]] = [val]
|
705
|
+
utilised[[un]] = nil
|
566
706
|
end
|
567
707
|
end
|
708
|
+
s
|
709
|
+
end
|
710
|
+
if defined_uniq.empty?
|
711
|
+
(starred - utilised.keys).each { |star| defined_uniq[[star]] = [cols.index(star)] }
|
712
|
+
# %%% puts "Tried to establish #{defined_uniq.inspect}"
|
568
713
|
end
|
569
|
-
(starred - utilised.keys).each { |star| defined_uniq[[star]] = [cols.index(star)] }
|
570
714
|
@defined_uniques[col_list] = defined_uniq
|
571
715
|
end
|
572
716
|
defined_uniq
|
573
717
|
end
|
574
718
|
end # module ClassMethods
|
575
719
|
|
720
|
+
# Called before building any object linked through a has_one or has_many so that foreign key IDs
|
721
|
+
# can be added properly to those new objects. Finally at the end also called to save everything.
|
722
|
+
def self._save_pending(to_be_saved)
|
723
|
+
while (tbs = to_be_saved.pop)
|
724
|
+
ais = (tbs.first.class.respond_to?(:around_import_save) && tbs.first.class.method(:around_import_save)) ||
|
725
|
+
(respond_to?(:around_import_save) && method(:around_import_save))
|
726
|
+
if ais
|
727
|
+
# Send them the sub_obj even if it might be invalid so they can choose
|
728
|
+
# to make it valid if they wish.
|
729
|
+
ais.call(tbs.first) do |modded_obj = nil|
|
730
|
+
modded_obj = (modded_obj || tbs.first)
|
731
|
+
modded_obj.save if modded_obj&.valid?
|
732
|
+
end
|
733
|
+
elsif tbs.first.valid?
|
734
|
+
tbs.first.save
|
735
|
+
else
|
736
|
+
puts "* Unable to save #{tbs.first.inspect}"
|
737
|
+
end
|
738
|
+
# puts "Save #{tbs.first.class.name} #{tbs.first&.id} #{!tbs.first.new_record?}"
|
739
|
+
unless tbs[1].nil? || tbs.first.new_record?
|
740
|
+
# puts "Calling #{tbs[1].class.name} #{tbs[1]&.id} .#{tbs[2]} #{tbs[3].class.name} #{tbs[3]&.id}"
|
741
|
+
tbs[1].send(tbs[2], tbs[3])
|
742
|
+
end
|
743
|
+
end
|
744
|
+
end
|
745
|
+
|
576
746
|
# The snake-cased column alias names used in the query to export data
|
577
747
|
def self._template_columns(klass, import_template = nil)
|
578
748
|
template_detail_columns = klass.instance_variable_get(:@template_detail_columns)
|
579
|
-
if
|
580
|
-
klass.instance_variable_set(:@template_import_columns,
|
749
|
+
if klass.instance_variable_get(:@template_import_columns) != import_template
|
750
|
+
klass.instance_variable_set(:@template_import_columns, import_template)
|
581
751
|
klass.instance_variable_set(:@template_detail_columns, (template_detail_columns = nil))
|
582
752
|
end
|
583
753
|
unless template_detail_columns
|
584
|
-
puts "* Redoing *"
|
754
|
+
# puts "* Redoing *"
|
585
755
|
template_detail_columns = _recurse_def(klass, import_template[:all], import_template).first.map(&:to_sym)
|
586
756
|
klass.instance_variable_set(:@template_detail_columns, template_detail_columns)
|
587
757
|
end
|
@@ -590,16 +760,21 @@ module DutyFree
|
|
590
760
|
|
591
761
|
# Recurse and return two arrays -- one with all columns in sequence, and one a hierarchy of
|
592
762
|
# nested hashes to be used with ActiveRecord's .joins() to facilitate export.
|
593
|
-
def self._recurse_def(klass, array, import_template, assocs = [], joins = [], pre_prefix = '', prefix = '')
|
763
|
+
def self._recurse_def(klass, array, import_template, order_by = [], assocs = [], joins = [], pre_prefix = '', prefix = '')
|
764
|
+
prefixes = ::DutyFree::Util._prefix_join([pre_prefix, prefix])
|
594
765
|
# Confirm we can actually navigate through this association
|
595
766
|
prefix_assoc = (assocs.last&.klass || klass).reflect_on_association(prefix) if prefix.present?
|
596
|
-
|
597
|
-
|
767
|
+
if prefix_assoc
|
768
|
+
assocs = assocs.dup << prefix_assoc
|
769
|
+
if prefix_assoc.macro == :has_many && (pk = prefix_assoc.active_record.primary_key)
|
770
|
+
order_by << ["#{prefixes.tr('.', '_')}_", pk]
|
771
|
+
end
|
772
|
+
end
|
598
773
|
array = array.inject([]) do |s, col|
|
599
774
|
s + if col.is_a?(Hash)
|
600
775
|
col.inject([]) do |s2, v|
|
601
776
|
joins << { v.first.to_sym => (joins_array = []) }
|
602
|
-
s2
|
777
|
+
s2 + _recurse_def(klass, (v.last.is_a?(Array) ? v.last : [v.last]), import_template, order_by, assocs, joins_array, prefixes, v.first.to_sym).first
|
603
778
|
end
|
604
779
|
elsif col.nil?
|
605
780
|
if assocs.empty?
|
@@ -608,7 +783,7 @@ module DutyFree
|
|
608
783
|
# Bring in from another class
|
609
784
|
joins << { prefix => (joins_array = []) }
|
610
785
|
# %%% Also bring in uniques and requireds
|
611
|
-
_recurse_def(klass, assocs.last.klass::IMPORT_TEMPLATE[:all], import_template, assocs, joins_array, prefixes).first
|
786
|
+
_recurse_def(klass, assocs.last.klass::IMPORT_TEMPLATE[:all], import_template, order_by, assocs, joins_array, prefixes).first
|
612
787
|
end
|
613
788
|
else
|
614
789
|
[::DutyFree::Column.new(col, pre_prefix, prefix, assocs, klass, import_template[:as])]
|
@@ -618,9 +793,13 @@ module DutyFree
|
|
618
793
|
end
|
619
794
|
end # module Extensions
|
620
795
|
|
621
|
-
|
796
|
+
# Rails < 4.0 doesn't have ActiveRecord::RecordNotUnique, so use the more generic ActiveRecord::ActiveRecordError instead
|
797
|
+
ar_not_unique_error = ActiveRecord.const_defined?('RecordNotUnique') ? ActiveRecord::RecordNotUnique : ActiveRecord::ActiveRecordError
|
798
|
+
class NoUniqueColumnError < ar_not_unique_error
|
622
799
|
end
|
623
800
|
|
624
|
-
|
801
|
+
# Rails < 4.2 doesn't have ActiveRecord::RecordInvalid, so use the more generic ActiveRecord::ActiveRecordError instead
|
802
|
+
ar_invalid_error = ActiveRecord.const_defined?('RecordInvalid') ? ActiveRecord::RecordInvalid : ActiveRecord::ActiveRecordError
|
803
|
+
class LessThanHalfAreMatchingColumnsError < ar_invalid_error
|
625
804
|
end
|
626
805
|
end
|