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