duty_free 1.0.0 → 1.0.5
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 +130 -0
- data/lib/duty_free/column.rb +9 -11
- data/lib/duty_free/extensions.rb +384 -195
- data/lib/duty_free/suggest_template.rb +23 -19
- data/lib/duty_free/util.rb +24 -10
- data/lib/duty_free/version_number.rb +1 -1
- data/lib/generators/duty_free/USAGE +1 -1
- data/lib/generators/duty_free/install_generator.rb +8 -10
- 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: 51bb54397528578ae7d4d43a04d1d6c31f81d1417d9c076f1fb751b18c09afaa
|
4
|
+
data.tar.gz: 8561d19c0a0166462c011f6a310429944de9b95751dbf9b36dbbb32fdce4bf76
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 02ae8681512885e5ab618aaef343ae88375338fc396ec711b28db78bd97daad361949b86c18c1ca81401e3a15f6629814900a751145bf8a270e50f1fa5669cab
|
7
|
+
data.tar.gz: 0444426a5468f3dcf7143f3f8859f79f2808db968b6a59561ff0351223993b0071aa94e95b4e50924d99ccbf4ae78f7a9b190a617eac6c21305d8ae0e7dd7f87
|
data/lib/duty_free.rb
CHANGED
@@ -62,7 +62,137 @@ module DutyFree
|
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
65
|
+
# Major compatibility fixes for ActiveRecord < 4.2
|
66
|
+
# ================================================
|
65
67
|
ActiveSupport.on_load(:active_record) do
|
68
|
+
# ActiveRecord before 4.0 didn't have #version
|
69
|
+
unless ActiveRecord.respond_to?(:version)
|
70
|
+
module ActiveRecord
|
71
|
+
def self.version
|
72
|
+
::Gem::Version.new(ActiveRecord::VERSION::STRING)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Rails < 4.0 cannot do #find_by, or do #pluck on multiple columns, so here are the patches:
|
78
|
+
if ActiveRecord.version < ::Gem::Version.new('4.0')
|
79
|
+
module ActiveRecord
|
80
|
+
module Calculations # Normally find_by is in FinderMethods, which older AR doesn't have
|
81
|
+
def find_by(*args)
|
82
|
+
where(*args).limit(1).to_a.first
|
83
|
+
end
|
84
|
+
|
85
|
+
def pluck(*column_names)
|
86
|
+
column_names.map! do |column_name|
|
87
|
+
if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s)
|
88
|
+
"#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
|
89
|
+
else
|
90
|
+
column_name
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Same as: if has_include?(column_names.first)
|
95
|
+
if eager_loading? || (includes_values.present? && (column_names.first || references_eager_loaded_tables?))
|
96
|
+
construct_relation_for_association_calculations.pluck(*column_names)
|
97
|
+
else
|
98
|
+
relation = clone # spawn
|
99
|
+
relation.select_values = column_names
|
100
|
+
result = if respond_to?(:bind_values)
|
101
|
+
klass.connection.select_all(relation.arel, nil, bind_values)
|
102
|
+
else
|
103
|
+
klass.connection.select_all(relation.arel.to_sql, nil)
|
104
|
+
end
|
105
|
+
if result.empty?
|
106
|
+
[]
|
107
|
+
else
|
108
|
+
columns = result.first.keys.map do |key|
|
109
|
+
# rubocop:disable Style/SingleLineMethods Naming/MethodParameterName
|
110
|
+
klass.columns_hash.fetch(key) do
|
111
|
+
Class.new { def type_cast(v); v; end }.new
|
112
|
+
end
|
113
|
+
# rubocop:enable Style/SingleLineMethods Naming/MethodParameterName
|
114
|
+
end
|
115
|
+
|
116
|
+
result = result.map do |attributes|
|
117
|
+
values = klass.initialize_attributes(attributes).values
|
118
|
+
|
119
|
+
columns.zip(values).map do |column, value|
|
120
|
+
column.type_cast(value)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
columns.one? ? result.map!(&:first) : result
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
unless Base.is_a?(Calculations)
|
130
|
+
class Base
|
131
|
+
class << self
|
132
|
+
delegate :pluck, :find_by, to: :scoped
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# ActiveRecord < 3.2 doesn't have initialize_attributes, used by .pluck()
|
138
|
+
unless AttributeMethods.const_defined?('Serialization')
|
139
|
+
class Base
|
140
|
+
class << self
|
141
|
+
def initialize_attributes(attributes, options = {}) #:nodoc:
|
142
|
+
serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
|
143
|
+
# super(attributes, options)
|
144
|
+
|
145
|
+
serialized_attributes.each do |key, coder|
|
146
|
+
attributes[key] = Attribute.new(coder, attributes[key], serialized) if attributes.key?(key)
|
147
|
+
end
|
148
|
+
|
149
|
+
attributes
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# This only gets added for ActiveRecord < 3.2
|
156
|
+
module Reflection
|
157
|
+
unless AssociationReflection.instance_methods.include?(:foreign_key)
|
158
|
+
class AssociationReflection < MacroReflection
|
159
|
+
alias foreign_key association_foreign_key
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Rails < 4.2 is not innately compatible with Ruby 2.4 and later, and comes up with:
|
167
|
+
# "TypeError: Cannot visit Integer" unless we patch like this:
|
168
|
+
unless ::Gem::Version.new(RUBY_VERSION) < ::Gem::Version.new('2.4')
|
169
|
+
unless Arel::Visitors::DepthFirst.private_instance_methods.include?(:visit_Integer)
|
170
|
+
module Arel
|
171
|
+
module Visitors
|
172
|
+
class DepthFirst < Visitor
|
173
|
+
alias visit_Integer terminal
|
174
|
+
end
|
175
|
+
|
176
|
+
class Dot < Visitor
|
177
|
+
alias visit_Integer visit_String
|
178
|
+
end
|
179
|
+
|
180
|
+
class ToSql < Visitor
|
181
|
+
private
|
182
|
+
|
183
|
+
# ActiveRecord before v3.2 uses Arel < 3.x, which does not have Arel#literal.
|
184
|
+
unless private_instance_methods.include?(:literal)
|
185
|
+
def literal(obj)
|
186
|
+
obj
|
187
|
+
end
|
188
|
+
end
|
189
|
+
alias visit_Integer literal
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
66
196
|
include ::DutyFree::Extensions
|
67
197
|
end
|
68
198
|
|
data/lib/duty_free/column.rb
CHANGED
@@ -5,15 +5,15 @@ require 'duty_free/util'
|
|
5
5
|
module DutyFree
|
6
6
|
# Holds detail about each column as we recursively explore the scope of what to import
|
7
7
|
class Column
|
8
|
-
attr_accessor :name, :pre_prefix, :prefix, :prefix_assocs, :
|
8
|
+
attr_accessor :name, :pre_prefix, :prefix, :prefix_assocs, :import_template_as
|
9
9
|
attr_writer :obj_class
|
10
10
|
|
11
|
-
def initialize(name, pre_prefix, prefix, prefix_assocs, obj_class,
|
11
|
+
def initialize(name, pre_prefix, prefix, prefix_assocs, obj_class, import_template_as)
|
12
12
|
self.name = name
|
13
13
|
self.pre_prefix = pre_prefix
|
14
14
|
self.prefix = prefix
|
15
15
|
self.prefix_assocs = prefix_assocs
|
16
|
-
self.
|
16
|
+
self.import_template_as = import_template_as
|
17
17
|
self.obj_class = obj_class
|
18
18
|
end
|
19
19
|
|
@@ -28,27 +28,25 @@ 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
|
-
[pre_prefix, prefix, ::DutyFree::Util._clean_name(name,
|
49
|
+
[pre_prefix, prefix, ::DutyFree::Util._clean_name(name, import_template_as)],
|
52
50
|
'_'
|
53
51
|
).tr('.', '_')
|
54
52
|
end
|
data/lib/duty_free/extensions.rb
CHANGED
@@ -2,10 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'duty_free/column'
|
4
4
|
require 'duty_free/suggest_template'
|
5
|
-
# require 'duty_free/attribute_serializers/object_attribute'
|
6
|
-
# require 'duty_free/attribute_serializers/object_changes_attribute'
|
7
5
|
# require 'duty_free/model_config'
|
8
|
-
# require 'duty_free/record_trail'
|
9
6
|
|
10
7
|
# :nodoc:
|
11
8
|
module DutyFree
|
@@ -17,36 +14,63 @@ module DutyFree
|
|
17
14
|
|
18
15
|
# :nodoc:
|
19
16
|
module ClassMethods
|
17
|
+
MAX_ID = Arel.sql('MAX(id)')
|
20
18
|
# def self.extended(model)
|
21
19
|
# end
|
22
20
|
|
23
21
|
# Export at least column header, and optionally include all existing data as well
|
24
|
-
def df_export(is_with_data = true,
|
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)
|
25
24
|
# In case they are only supplying the columns hash
|
26
|
-
if is_with_data.is_a?(Hash) && !
|
27
|
-
|
25
|
+
if is_with_data.is_a?(Hash) && !import_template
|
26
|
+
import_template = is_with_data
|
28
27
|
is_with_data = true
|
29
28
|
end
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
29
|
+
import_template ||= if constants.include?(:IMPORT_TEMPLATE)
|
30
|
+
self::IMPORT_TEMPLATE
|
31
|
+
else
|
32
|
+
suggest_template(0, false, false)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Friendly column names that end up in the first row of the CSV
|
36
|
+
# Required columns get prefixed with a *
|
37
|
+
requireds = (import_template[:required] || [])
|
38
|
+
rows = ::DutyFree::Extensions._template_columns(self, import_template).map do |col|
|
39
|
+
is_required = requireds.include?(col)
|
40
|
+
col = col.to_s.titleize
|
41
|
+
# Alias-ify the full column names
|
42
|
+
aliases = (import_template[:as] || [])
|
43
|
+
aliases.each do |k, v|
|
44
|
+
if col.start_with?(v)
|
45
|
+
col = k + col[v.length..-1]
|
46
|
+
break
|
47
|
+
end
|
48
|
+
end
|
49
|
+
(is_required ? '* ' : '') + col
|
50
|
+
end
|
51
|
+
rows = [rows]
|
52
|
+
|
36
53
|
if is_with_data
|
37
54
|
# Automatically create a JOINs strategy and select list to get back all related rows
|
38
|
-
template_cols, template_joins =
|
39
|
-
relation = joins(template_joins)
|
55
|
+
template_cols, template_joins = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template)
|
56
|
+
relation = use_inner_joins ? joins(template_joins) : left_joins(template_joins)
|
40
57
|
|
41
58
|
# So we can properly create the SELECT list, create a mapping between our
|
42
59
|
# column alias prefixes and the aliases AREL creates.
|
43
|
-
|
44
|
-
|
45
|
-
|
60
|
+
core = relation.arel.ast.cores.first
|
61
|
+
# Accommodate AR < 3.2
|
62
|
+
arel_alias_names = if core.froms.is_a?(Arel::Table)
|
63
|
+
# All recent versions of AR have #source which brings up an Arel::Nodes::JoinSource
|
64
|
+
::DutyFree::Util._recurse_arel(core.source)
|
65
|
+
else
|
66
|
+
# With AR < 3.2, "froms" brings up the top node, an Arel::Nodes::InnerJoin
|
67
|
+
::DutyFree::Util._recurse_arel(core.froms)
|
68
|
+
end
|
69
|
+
our_names = ['_'] + ::DutyFree::Util._recurse_arel(template_joins)
|
46
70
|
mapping = our_names.zip(arel_alias_names).to_h
|
47
71
|
|
48
72
|
relation.select(template_cols.map { |x| x.to_s(mapping) }).each do |result|
|
49
|
-
rows <<
|
73
|
+
rows << ::DutyFree::Extensions._template_columns(self, import_template).map do |col|
|
50
74
|
value = result.send(col)
|
51
75
|
case value
|
52
76
|
when true
|
@@ -63,12 +87,16 @@ module DutyFree
|
|
63
87
|
end
|
64
88
|
|
65
89
|
# With an array of incoming data, the first row having column names, perform the import
|
66
|
-
def df_import(data,
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
90
|
+
def df_import(data, import_template = nil)
|
91
|
+
instance_variable_set(:@defined_uniques, nil)
|
92
|
+
instance_variable_set(:@valid_uniques, nil)
|
93
|
+
|
94
|
+
import_template ||= if constants.include?(:IMPORT_TEMPLATE)
|
95
|
+
self::IMPORT_TEMPLATE
|
96
|
+
else
|
97
|
+
suggest_template(0, false, false)
|
98
|
+
end
|
99
|
+
# puts "Chose #{import_template}"
|
72
100
|
inserts = []
|
73
101
|
updates = []
|
74
102
|
counts = Hash.new { |h, k| h[k] = [] }
|
@@ -79,17 +107,19 @@ module DutyFree
|
|
79
107
|
cols = nil
|
80
108
|
starred = []
|
81
109
|
partials = []
|
82
|
-
all =
|
110
|
+
all = import_template[:all]
|
83
111
|
keepers = {}
|
84
112
|
valid_unique = nil
|
85
113
|
existing = {}
|
86
114
|
devise_class = ''
|
115
|
+
ret = nil
|
87
116
|
|
117
|
+
# Multi-tenancy gem Apartment can be used if there are separate schemas per tenant
|
88
118
|
reference_models = if Object.const_defined?('Apartment')
|
89
119
|
Apartment.excluded_models
|
90
120
|
else
|
91
121
|
[]
|
92
|
-
|
122
|
+
end
|
93
123
|
|
94
124
|
if Object.const_defined?('Devise')
|
95
125
|
Object.const_get('Devise') # If this fails, devise_class will remain a blank string.
|
@@ -101,10 +131,13 @@ module DutyFree
|
|
101
131
|
|
102
132
|
# Did they give us a filename?
|
103
133
|
if data.is_a?(String)
|
104
|
-
|
134
|
+
# Filenames with full paths can not be longer than 4096 characters, and can not
|
135
|
+
# include newline characters
|
136
|
+
data = if data.length <= 4096 && !data.index('\n')
|
105
137
|
File.open(data)
|
106
138
|
else
|
107
|
-
#
|
139
|
+
# Any multi-line string is likely CSV data
|
140
|
+
# %%% Test to see if TAB characters are present on the first line, instead of commas
|
108
141
|
CSV.new(data)
|
109
142
|
end
|
110
143
|
end
|
@@ -123,13 +156,20 @@ module DutyFree
|
|
123
156
|
# Will show as just one transaction when using auditing solutions such as PaperTrail
|
124
157
|
ActiveRecord::Base.transaction do
|
125
158
|
# Check to see if they want to do anything before the whole import
|
126
|
-
if
|
127
|
-
|
159
|
+
# First if defined in the import_template, then if there is a method in the class,
|
160
|
+
# and finally (not yet implemented) a generic global before_import
|
161
|
+
my_before_import = import_template[:before_import]
|
162
|
+
my_before_import ||= respond_to?(:before_import) && method(:before_import)
|
163
|
+
# my_before_import ||= some generic my_before_import
|
164
|
+
if my_before_import
|
165
|
+
last_arg_idx = my_before_import.parameters.length - 1
|
166
|
+
arguments = [data, import_template][0..last_arg_idx]
|
167
|
+
data = ret if (ret = my_before_import.call(*arguments)).is_a?(Enumerable)
|
128
168
|
end
|
129
169
|
data.each_with_index do |row, row_num|
|
130
170
|
row_errors = {}
|
131
171
|
if is_first # Anticipate that first row has column names
|
132
|
-
uniques =
|
172
|
+
uniques = import_template[:uniques]
|
133
173
|
|
134
174
|
# Look for UTF-8 BOM in very first cell
|
135
175
|
row[0] = row[0][3..-1] if row[0].start_with?([239, 187, 191].pack('U*'))
|
@@ -154,34 +194,37 @@ module DutyFree
|
|
154
194
|
col.strip!
|
155
195
|
end
|
156
196
|
end
|
157
|
-
|
158
|
-
cols
|
197
|
+
cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) }
|
198
|
+
defined_uniques(uniques, cols, cols.join('|'), starred)
|
159
199
|
# Make sure that at least half of them match what we know as being good column names
|
160
|
-
template_column_objects =
|
200
|
+
template_column_objects = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template).first
|
161
201
|
cols.each_with_index do |col, idx|
|
162
202
|
# prefixes = col_detail.pre_prefix + (col_detail.prefix.blank? ? [] : [col_detail.prefix])
|
163
203
|
keepers[idx] = template_column_objects.find { |col_obj| col_obj.titleize == col }
|
164
204
|
# puts "Could not find a match for column #{idx + 1}, #{col}" if keepers[idx].nil?
|
165
205
|
end
|
166
|
-
if keepers.length < (cols.length / 2) - 1
|
167
|
-
raise ::DutyFree::LessThanHalfAreMatchingColumnsError, I18n.t('import.altered_import_template_coumns')
|
168
|
-
end
|
206
|
+
raise ::DutyFree::LessThanHalfAreMatchingColumnsError, I18n.t('import.altered_import_template_coumns') if keepers.length < (cols.length / 2) - 1
|
169
207
|
|
170
208
|
# Returns just the first valid unique lookup set if there are multiple
|
171
|
-
valid_unique =
|
209
|
+
valid_unique = find_existing(uniques, cols, starred, import_template, keepers, false)
|
172
210
|
# Make a lookup from unique values to specific IDs
|
173
|
-
existing = pluck(*([:id] + valid_unique.keys)).each_with_object(existing)
|
211
|
+
existing = pluck(*([:id] + valid_unique.keys)).each_with_object(existing) do |v, s|
|
212
|
+
s[v[1..-1].map(&:to_s)] = v.first
|
213
|
+
s
|
214
|
+
end
|
174
215
|
is_first = false
|
175
216
|
else # Normal row of data
|
176
217
|
is_insert = false
|
177
|
-
is_do_save = true
|
178
218
|
existing_unique = valid_unique.inject([]) do |s, v|
|
179
|
-
s <<
|
219
|
+
s << if v.last.is_a?(Array)
|
220
|
+
# binding.pry
|
221
|
+
v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck(MAX_ID).first.to_s
|
222
|
+
else
|
223
|
+
row[v.last].to_s
|
224
|
+
end
|
180
225
|
end
|
181
226
|
# Check to see if they want to preprocess anything
|
182
|
-
if @before_process ||=
|
183
|
-
existing_unique = @before_process.call(valid_unique, existing_unique)
|
184
|
-
end
|
227
|
+
existing_unique = @before_process.call(valid_unique, existing_unique) if @before_process ||= import_template[:before_process]
|
185
228
|
obj = if existing.include?(existing_unique)
|
186
229
|
find(existing[existing_unique])
|
187
230
|
else
|
@@ -195,22 +238,26 @@ module DutyFree
|
|
195
238
|
sub_objects = {}
|
196
239
|
this_path = nil
|
197
240
|
keepers.each do |key, v|
|
198
|
-
klass = nil
|
199
241
|
next if v.nil?
|
200
242
|
|
201
243
|
# Not the same as the last path?
|
202
244
|
if this_path && v.path != this_path.split(',').map(&:to_sym) && !is_has_one
|
203
|
-
|
204
|
-
|
205
|
-
if
|
206
|
-
|
207
|
-
|
208
|
-
|
245
|
+
# puts sub_obj.class.name
|
246
|
+
if respond_to?(:around_import_save)
|
247
|
+
# Send them the sub_obj even if it might be invalid so they can choose
|
248
|
+
# to make it valid if they wish.
|
249
|
+
# binding.pry
|
250
|
+
around_import_save(sub_obj) do |modded_obj = nil|
|
251
|
+
modded_obj = (modded_obj || sub_obj)
|
252
|
+
modded_obj.save if sub_obj&.valid?
|
209
253
|
end
|
254
|
+
elsif sub_obj&.valid?
|
255
|
+
sub_obj.save
|
210
256
|
end
|
211
257
|
end
|
212
258
|
sub_obj = obj
|
213
|
-
this_path = ''
|
259
|
+
this_path = +''
|
260
|
+
# puts "p: #{v.path}"
|
214
261
|
v.path.each_with_index do |path_part, idx|
|
215
262
|
this_path << (this_path.blank? ? path_part.to_s : ",#{path_part}")
|
216
263
|
unless (sub_next = sub_objects[this_path])
|
@@ -233,59 +280,48 @@ module DutyFree
|
|
233
280
|
# This works for belongs_to or has_one. has_many gets sorted below.
|
234
281
|
# Get existing related object, or create a new one
|
235
282
|
if (sub_next = sub_obj.send(path_part)).nil?
|
236
|
-
is_has_one = assoc.is_a?(ActiveRecord::Reflection::HasOneReflection)
|
237
283
|
klass = Object.const_get(assoc&.class_name)
|
238
|
-
|
284
|
+
# assoc.is_a?(ActiveRecord::Reflection::HasOneReflection)
|
285
|
+
# %%% When we support only AR 4.2 and above then we can do: assoc.has_one?
|
286
|
+
sub_next = if assoc.macro == :has_one
|
239
287
|
has_ones << v.path
|
240
288
|
klass.new
|
241
289
|
else
|
242
290
|
# Try to find a unique item if one is referenced
|
243
|
-
|
291
|
+
sub_bt = nil
|
244
292
|
begin
|
245
|
-
|
293
|
+
trim_prefix = v.titleize[0..-(v.name.length + 2)]
|
294
|
+
trim_prefix << ' ' unless trim_prefix.blank?
|
295
|
+
sub_bt, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, nil, row, klass, all, trim_prefix)
|
246
296
|
rescue ::DutyFree::NoUniqueColumnError
|
247
|
-
sub_unique = nil
|
248
297
|
end
|
249
|
-
#
|
250
|
-
criteria
|
251
|
-
|
252
|
-
|
253
|
-
end
|
254
|
-
# Try looking up this belongs_to object through ActiveRecord
|
255
|
-
sub_bt = assoc.klass.find_by(criteria) if criteria
|
256
|
-
sub_bt || sub_obj.send("#{path_part}=", klass.new(criteria || {}))
|
298
|
+
# %%% Can criteria really ever be nil anymore?
|
299
|
+
sub_bt ||= klass.new(criteria || {}) unless klass == sub_obj.class && criteria.empty?
|
300
|
+
sub_obj.send("#{path_part}=", sub_bt)
|
301
|
+
sub_bt
|
257
302
|
end
|
258
303
|
end
|
259
304
|
# Look for possible missing polymorphic detail
|
305
|
+
# Maybe can test for this via assoc.through_reflection
|
260
306
|
if assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) &&
|
261
307
|
(delegate = assoc.send(:delegate_reflection)&.active_record&.reflect_on_association(assoc.source_reflection_name)) &&
|
262
308
|
delegate.options[:polymorphic]
|
263
309
|
polymorphics << { parent: sub_next, child: sub_obj, type_col: delegate.foreign_type, id_col: delegate.foreign_key.to_s }
|
264
310
|
end
|
265
311
|
# From a has_many?
|
266
|
-
|
312
|
+
# Rails 4.0 and later can do: sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
|
313
|
+
if assoc.macro == :has_many && !assoc.options[:through]
|
267
314
|
# Try to find a unique item if one is referenced
|
268
315
|
# %%% There is possibility that when bringing in related classes using a nil
|
269
|
-
# in
|
316
|
+
# in IMPORT_TEMPLATE[:all] that this will break. Need to test deeply nested things.
|
270
317
|
start = (v.pre_prefix.blank? ? 0 : v.pre_prefix.length)
|
271
318
|
trim_prefix = v.titleize[start..-(v.name.length + 2)]
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
sub_hm = sub_next.find do |hm_obj|
|
277
|
-
is_good = true
|
278
|
-
criteria.each do |k, v|
|
279
|
-
if hm_obj.send(k).to_s != v.to_s
|
280
|
-
is_good = false
|
281
|
-
break
|
282
|
-
end
|
283
|
-
end
|
284
|
-
is_good
|
285
|
-
end
|
286
|
-
# Try looking it up through ActiveRecord
|
287
|
-
sub_hm = sub_next.find_by(criteria) if sub_hm.nil?
|
319
|
+
trim_prefix << ' ' unless trim_prefix.blank?
|
320
|
+
# assoc.inverse_of is the belongs_to side of the has_many train we came in here on.
|
321
|
+
sub_hm, criteria = assoc.klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
|
322
|
+
|
288
323
|
# If still not found then create a new related object using this has_many collection
|
324
|
+
# (criteria.empty? ? nil : sub_next.new(criteria))
|
289
325
|
sub_next = sub_hm || sub_next.new(criteria)
|
290
326
|
end
|
291
327
|
unless sub_next.nil?
|
@@ -300,25 +336,23 @@ module DutyFree
|
|
300
336
|
sub_objects[this_path] = sub_next if this_path.present?
|
301
337
|
end
|
302
338
|
end
|
303
|
-
sub_obj = sub_next
|
339
|
+
sub_obj = sub_next # if sub_next
|
304
340
|
end
|
305
341
|
next if sub_obj.nil?
|
306
342
|
|
307
|
-
sym = "#{v.name}=".to_sym
|
308
|
-
sub_class = sub_obj.class
|
309
|
-
next unless sub_obj.respond_to?(sym)
|
343
|
+
next unless sub_obj.respond_to?(sym = "#{v.name}=".to_sym)
|
310
344
|
|
311
|
-
col_type =
|
312
|
-
if col_type.nil? && (virtual_columns =
|
345
|
+
col_type = sub_obj.class.columns_hash[v.name.to_s]&.type
|
346
|
+
if col_type.nil? && (virtual_columns = import_template[:virtual_columns]) &&
|
313
347
|
(virtual_columns = virtual_columns[this_path] || virtual_columns)
|
314
348
|
col_type = virtual_columns[v.name]
|
315
349
|
end
|
316
350
|
if col_type == :boolean
|
317
351
|
if row[key].nil?
|
318
352
|
# Do nothing when it's nil
|
319
|
-
elsif %w[yes y].include?(row[key]&.downcase) # Used to cover '
|
353
|
+
elsif %w[true t yes y].include?(row[key]&.strip&.downcase) # Used to cover 'on'
|
320
354
|
row[key] = true
|
321
|
-
elsif %w[no n].include?(row[key]&.downcase) # Used to cover '
|
355
|
+
elsif %w[false f no n].include?(row[key]&.strip&.downcase) # Used to cover 'off'
|
322
356
|
row[key] = false
|
323
357
|
else
|
324
358
|
row_errors[v.name] ||= []
|
@@ -327,7 +361,7 @@ module DutyFree
|
|
327
361
|
end
|
328
362
|
sub_obj.send(sym, row[key])
|
329
363
|
# else
|
330
|
-
# puts " #{
|
364
|
+
# puts " #{sub_obj.class.name} doesn't respond to #{sym}"
|
331
365
|
end
|
332
366
|
# Try to save a final sub-object if one exists
|
333
367
|
sub_obj.save if sub_obj && this_path && !is_has_one && sub_obj.valid?
|
@@ -347,13 +381,18 @@ module DutyFree
|
|
347
381
|
end
|
348
382
|
end
|
349
383
|
|
350
|
-
# Give a window of
|
351
|
-
|
384
|
+
# Give a window of opportunity to tweak user objects controlled by Devise
|
385
|
+
obj_class = obj.class
|
386
|
+
is_do_save = if obj_class.respond_to?(:before_devise_save) && obj_class.name == devise_class
|
387
|
+
obj_class.before_devise_save(obj, existing)
|
388
|
+
else
|
389
|
+
true
|
390
|
+
end
|
352
391
|
|
353
392
|
if obj.valid?
|
354
393
|
obj.save if is_do_save
|
355
394
|
# Snag back any changes to the unique columns. (For instance, Devise downcases email addresses.)
|
356
|
-
existing_unique = valid_unique.keys.inject([]) { |s, v| s << obj.send(v) }
|
395
|
+
existing_unique = valid_unique.keys.inject([]) { |s, v| s << obj.send(v).to_s }
|
357
396
|
# Update the duplicate counts and inserted / updated results
|
358
397
|
counts[existing_unique] << row_num
|
359
398
|
(is_insert ? inserts : updates) << { row_num => existing_unique } if is_do_save
|
@@ -365,106 +404,233 @@ module DutyFree
|
|
365
404
|
errors << { row_num => row_errors } unless row_errors.empty?
|
366
405
|
end
|
367
406
|
end
|
368
|
-
duplicates = counts.
|
369
|
-
s
|
407
|
+
duplicates = counts.each_with_object([]) do |v, s|
|
408
|
+
s += v.last[1..-1].map { |line_num| { line_num => v.first } } if v.last.count > 1
|
409
|
+
s
|
370
410
|
end
|
371
|
-
# Check to see if they want to do anything before the whole import
|
372
411
|
ret = { inserted: inserts, updated: updates, duplicates: duplicates, errors: errors }
|
373
|
-
|
374
|
-
|
412
|
+
|
413
|
+
# Check to see if they want to do anything after the import
|
414
|
+
# First if defined in the import_template, then if there is a method in the class,
|
415
|
+
# and finally (not yet implemented) a generic global after_import
|
416
|
+
my_after_import = import_template[:after_import]
|
417
|
+
my_after_import ||= respond_to?(:after_import) && method(:after_import)
|
418
|
+
# my_after_import ||= some generic my_after_import
|
419
|
+
if my_after_import
|
420
|
+
last_arg_idx = my_after_import.parameters.length - 1
|
421
|
+
arguments = [ret][0..last_arg_idx]
|
422
|
+
ret = ret2 if (ret2 = my_after_import.call(*arguments)).is_a?(Hash)
|
375
423
|
end
|
376
424
|
end
|
377
425
|
ret
|
378
426
|
end
|
379
427
|
|
380
|
-
#
|
381
|
-
#
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
if col.start_with?(v)
|
391
|
-
col = k + col[v.length..-1]
|
392
|
-
break
|
393
|
-
end
|
428
|
+
# For use with importing, based on the provided column list calculate all valid combinations
|
429
|
+
# of unique columns. If there is no valid combination, throws an error.
|
430
|
+
# Returns an object found by this means.
|
431
|
+
def find_existing(uniques, cols, starred, import_template, keepers, train_we_came_in_here_on,
|
432
|
+
row = nil, klass_or_collection = nil, all = nil, trim_prefix = '')
|
433
|
+
unless trim_prefix.blank?
|
434
|
+
cols = cols.map { |c| c.start_with?(trim_prefix) ? c[trim_prefix.length..-1] : nil }
|
435
|
+
starred = starred.each_with_object([]) do |v, s|
|
436
|
+
s << v[trim_prefix.length..-1] if v.start_with?(trim_prefix)
|
437
|
+
s
|
394
438
|
end
|
395
|
-
(is_required ? '* ' : '') + col
|
396
439
|
end
|
397
|
-
|
440
|
+
col_list = cols.join('|')
|
398
441
|
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
442
|
+
# First add in foreign key stuff we can find from belongs_to associations (other than the
|
443
|
+
# one we might have arrived here upon).
|
444
|
+
criteria = {} # Enough detail to find or build a new object
|
445
|
+
bt_criteria = {}
|
446
|
+
bt_criteria_all_nil = true
|
447
|
+
bt_col_indexes = []
|
448
|
+
available_bts = []
|
449
|
+
only_valid_uniques = (train_we_came_in_here_on == false)
|
450
|
+
uniq_lookups = {} # The data, or how to look up the data
|
407
451
|
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
@valid_uniques ||= {} # Fancy memoisation
|
413
|
-
col_list = cols.join('|')
|
414
|
-
unless (vus = @valid_uniques[col_list])
|
452
|
+
vus = ((@valid_uniques ||= {})[col_list] ||= {}) # Fancy memoisation
|
453
|
+
|
454
|
+
if (is_new_vus = vus.empty?)
|
455
|
+
# # Let's do general attributes before the tricky foreign key stuff
|
415
456
|
# Find all unique combinations that are available based on incoming columns, and
|
416
457
|
# pair them up with column number mappings.
|
417
|
-
template_column_objects =
|
458
|
+
template_column_objects = ::DutyFree::Extensions._recurse_def(self, all || import_template[:all], import_template).first
|
418
459
|
available = if trim_prefix.blank?
|
419
460
|
template_column_objects.select { |col| col.pre_prefix.blank? && col.prefix.blank? }
|
420
461
|
else
|
421
462
|
trim_prefix_snake = trim_prefix.downcase.tr(' ', '_')
|
422
463
|
template_column_objects.select do |col|
|
423
|
-
|
464
|
+
this_prefix = ::DutyFree::Util._prefix_join([col.pre_prefix, col.prefix], '_').tr('.', '_')
|
465
|
+
trim_prefix_snake == "#{this_prefix}_"
|
424
466
|
end
|
425
467
|
end.map { |avail| avail.name.to_s.titleize }
|
426
|
-
|
468
|
+
all_vus = defined_uniques(uniques, cols, nil, starred, trim_prefix)
|
469
|
+
# k, v = all_vus.first
|
470
|
+
# k.each_with_index do |col, idx|
|
471
|
+
# if available.include?(col) # || available_bts.include?(col)
|
472
|
+
# vus[col] ||= v[idx]
|
473
|
+
# end
|
474
|
+
# # if available_bts.include?(k)
|
475
|
+
end
|
476
|
+
|
477
|
+
# %%% Ultimately may consider making this recursive
|
478
|
+
reflect_on_all_associations.each do |sn_bt|
|
479
|
+
next unless sn_bt.belongs_to? && (!train_we_came_in_here_on || sn_bt != train_we_came_in_here_on)
|
480
|
+
|
481
|
+
# # %%% Make sure there's a starred column we know about from this one
|
482
|
+
# uniq_lookups[sn_bt.foreign_key] = nil if only_valid_uniques
|
483
|
+
|
484
|
+
# This search prefix becomes something like "Order Details Product "
|
485
|
+
cols.each_with_index do |bt_col, idx|
|
486
|
+
next if bt_col_indexes.include?(idx) ||
|
487
|
+
!bt_col&.start_with?(bt_prefix = (trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} "))
|
488
|
+
|
489
|
+
available_bts << bt_col
|
490
|
+
fk_id = if row
|
491
|
+
# Max ID so if there are multiple, only the most recent one is picked.
|
492
|
+
# %%% Need to stack these up in case there are multiple
|
493
|
+
# (like first_name, last_name on a referenced employee)
|
494
|
+
sn_bt.klass.where(keepers[idx].name => row[idx]).limit(1).pluck(MAX_ID).first
|
495
|
+
else
|
496
|
+
# elsif is_new_vus
|
497
|
+
# # Add to our criteria if this belongs_to is required
|
498
|
+
# bt_req_by_default = sn_bt.klass.respond_to?(:belongs_to_required_by_default) &&
|
499
|
+
# sn_bt.klass.belongs_to_required_by_default
|
500
|
+
# unless !vus.values.first&.include?(idx) &&
|
501
|
+
# (sn_bt.options[:optional] || (sn_bt.options[:required] == false) || !bt_req_by_default)
|
502
|
+
# # # Add this fk to the criteria
|
503
|
+
# # criteria[fk_name] = fk_id
|
504
|
+
|
505
|
+
# ref = [keepers[idx].name, idx]
|
506
|
+
# # bt_criteria[(fk_name = sn_bt.foreign_key)] ||= [sn_bt.klass, []]
|
507
|
+
# # bt_criteria[fk_name].last << ref
|
508
|
+
# # bt_criteria[bt_col] = [sn_bt.klass, ref]
|
509
|
+
|
510
|
+
# # Maybe this is the most useful
|
511
|
+
# # First array is friendly column names, second is references
|
512
|
+
# foreign_uniques = (bt_criteria[sn_bt.name] ||= [sn_bt.klass, [], []])
|
513
|
+
# foreign_uniques[1] << ref
|
514
|
+
# foreign_uniques[2] << bt_col
|
515
|
+
# vus[bt_col] = foreign_uniques # And we can look up this growing set from any foreign column
|
516
|
+
[sn_bt.klass, keepers[idx].name, idx]
|
517
|
+
end
|
518
|
+
if fk_id
|
519
|
+
bt_col_indexes << idx
|
520
|
+
bt_criteria_all_nil = false
|
521
|
+
end
|
522
|
+
bt_criteria[(fk_name = sn_bt.foreign_key)] = fk_id
|
523
|
+
|
524
|
+
# Add to our criteria if this belongs_to is required
|
525
|
+
bt_req_by_default = sn_bt.klass.respond_to?(:belongs_to_required_by_default) &&
|
526
|
+
sn_bt.klass.belongs_to_required_by_default
|
527
|
+
|
528
|
+
# The first check, "!all_vus.keys.first.exists { |k| k.start_with?(bt_prefix) }"
|
529
|
+
# is to see if one of the columns we're working with from the unique that we've chosen
|
530
|
+
# comes from the table referenced by this belongs_to (sn_bt).
|
531
|
+
next if all_vus.keys.first.none? { |k| k.start_with?(bt_prefix) } &&
|
532
|
+
(sn_bt.options[:optional] || !bt_req_by_default)
|
533
|
+
|
534
|
+
# Add to the criteria
|
535
|
+
criteria[fk_name] = fk_id
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
if is_new_vus
|
540
|
+
available += available_bts
|
541
|
+
all_vus.each do |k, v|
|
542
|
+
combined_k = []
|
543
|
+
combined_v = []
|
544
|
+
k.each_with_index do |key, idx|
|
545
|
+
if available.include?(key)
|
546
|
+
combined_k << key
|
547
|
+
combined_v << v[idx]
|
548
|
+
end
|
549
|
+
end
|
550
|
+
vus[combined_k] = combined_v unless combined_k.empty?
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
# uniq_lookups = vus.inject({}) do |s, v|
|
555
|
+
# return s if available_bts.include?(v.first) # These will be provided in criteria, and not uniq_lookups
|
556
|
+
|
557
|
+
# # uniq_lookups[k[trim_prefix.length..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
|
558
|
+
# s[v.first.downcase.tr(' ', '_').to_sym] = v.last
|
559
|
+
# s
|
560
|
+
# end
|
561
|
+
|
562
|
+
new_criteria_all_nil = bt_criteria_all_nil
|
563
|
+
|
564
|
+
# Make sure they have at least one unique combination to take cues from
|
565
|
+
unless vus.empty? # raise NoUniqueColumnError.new(I18n.t('import.no_unique_column_error'))
|
566
|
+
# Convert the first entry to a simplified hash, such as:
|
567
|
+
# {[:investigator_institutions_name, :investigator_institutions_email] => [8, 9], ...}
|
568
|
+
# to {:name => 8, :email => 9}
|
569
|
+
key, val = vus.first # Utilise the first identified set of valid uniques
|
570
|
+
key.each_with_index do |k, idx|
|
571
|
+
next if available_bts.include?(k) # These will be provided in criteria, and not uniq_lookups
|
572
|
+
|
573
|
+
# uniq_lookups[k[trim_prefix.length..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
|
574
|
+
k_sym = k.downcase.tr(' ', '_').to_sym
|
575
|
+
v = val[idx]
|
576
|
+
uniq_lookups[k_sym] = v # The column number in which to find the data
|
577
|
+
|
578
|
+
next if only_valid_uniques || bt_col_indexes.include?(v)
|
579
|
+
|
580
|
+
# Find by all corresponding columns
|
581
|
+
if (row_value = row[v])
|
582
|
+
new_criteria_all_nil = false
|
583
|
+
criteria[k_sym] = row_value # The data, or how to look up the data
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
# Short-circuiting this to only get back the valid_uniques?
|
589
|
+
# unless uniq_lookups == criteria
|
590
|
+
# puts "Compare #{uniq_lookups.inspect}"
|
591
|
+
# puts "Compare #{criteria.inspect}"
|
592
|
+
# end
|
593
|
+
return uniq_lookups.merge(criteria) if only_valid_uniques
|
594
|
+
|
595
|
+
# If there's nothing to match upon then we're out
|
596
|
+
return [nil, {}] if new_criteria_all_nil
|
597
|
+
|
598
|
+
# With this criteria, find any matching has_many row we can so we can update it
|
599
|
+
# First try looking it up through ActiveRecord
|
600
|
+
found_object = klass_or_collection.find_by(criteria)
|
601
|
+
# If not successful, such as when fields are exposed via helper methods instead of being
|
602
|
+
# real columns in the database tables, try this more intensive routine.
|
603
|
+
unless found_object || klass_or_collection.is_a?(Array)
|
604
|
+
found_object = klass_or_collection.find do |obj|
|
427
605
|
is_good = true
|
428
|
-
|
429
|
-
|
606
|
+
criteria.each do |k, v|
|
607
|
+
if obj.send(k).to_s != v.to_s
|
430
608
|
is_good = false
|
431
609
|
break
|
432
610
|
end
|
433
611
|
end
|
434
612
|
is_good
|
435
613
|
end
|
436
|
-
@valid_uniques[col_list] = vus
|
437
|
-
end
|
438
|
-
|
439
|
-
# Make sure they have at least one unique combination to take cues from
|
440
|
-
raise ::DutyFree::NoUniqueColumnError, I18n.t('import.no_unique_column_error') if vus.empty?
|
441
|
-
|
442
|
-
# Convert the first entry to a simplified hash, such as:
|
443
|
-
# {[:investigator_institutions_name, :investigator_institutions_email] => [8, 9], ...}
|
444
|
-
# to {:name => 8, :email => 9}
|
445
|
-
key, val = vus.first
|
446
|
-
ret = {}
|
447
|
-
key.each_with_index do |k, idx|
|
448
|
-
ret[k[col_name_offset..-1].downcase.tr(' ', '_').to_sym] = val[idx]
|
449
614
|
end
|
450
|
-
|
615
|
+
[found_object, criteria.merge(bt_criteria)]
|
451
616
|
end
|
452
617
|
|
453
618
|
private
|
454
619
|
|
455
|
-
def defined_uniques(uniques, cols = [], starred = [])
|
456
|
-
|
457
|
-
unless (
|
620
|
+
def defined_uniques(uniques, cols = [], col_list = nil, starred = [], trim_prefix = '')
|
621
|
+
col_list ||= cols.join('|')
|
622
|
+
unless (defined_uniq = (@defined_uniques ||= {})[col_list])
|
458
623
|
utilised = {} # Track columns that have been referenced thusfar
|
459
|
-
|
624
|
+
defined_uniq = uniques.each_with_object({}) do |unique, s|
|
460
625
|
if unique.is_a?(Array)
|
461
626
|
key = []
|
462
627
|
value = []
|
463
628
|
unique.each do |unique_part|
|
464
|
-
val =
|
465
|
-
|
629
|
+
val = (unique_part_name = unique_part.to_s.titleize).start_with?(trim_prefix) &&
|
630
|
+
cols.index(upn = unique_part_name[trim_prefix.length..-1])
|
631
|
+
next unless val
|
466
632
|
|
467
|
-
key <<
|
633
|
+
key << upn
|
468
634
|
value << val
|
469
635
|
end
|
470
636
|
unless key.empty?
|
@@ -472,54 +638,77 @@ module DutyFree
|
|
472
638
|
utilised[key] = nil
|
473
639
|
end
|
474
640
|
else
|
475
|
-
val =
|
476
|
-
|
477
|
-
|
478
|
-
|
641
|
+
val = (unique_name = unique.to_s.titleize).start_with?(trim_prefix) &&
|
642
|
+
cols.index(un = unique_name[trim_prefix.length..-1])
|
643
|
+
if val
|
644
|
+
s[[un]] = [val]
|
645
|
+
utilised[[un]] = nil
|
479
646
|
end
|
480
647
|
end
|
648
|
+
s
|
649
|
+
end
|
650
|
+
if defined_uniq.empty?
|
651
|
+
(starred - utilised.keys).each { |star| defined_uniq[[star]] = [cols.index(star)] }
|
652
|
+
# %%% puts "Tried to establish #{defined_uniq.inspect}"
|
481
653
|
end
|
482
|
-
|
483
|
-
@defined_uniques[cols] = defined_uniques
|
654
|
+
@defined_uniques[col_list] = defined_uniq
|
484
655
|
end
|
485
|
-
|
656
|
+
defined_uniq
|
486
657
|
end
|
658
|
+
end # module ClassMethods
|
487
659
|
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
prefixes = ::DutyFree::Util._prefix_join([pre_prefix, prefix])
|
495
|
-
array = array.inject([]) do |s, col|
|
496
|
-
s += if col.is_a?(Hash)
|
497
|
-
col.inject([]) do |s2, v|
|
498
|
-
joins << { v.first.to_sym => (joins_array = []) }
|
499
|
-
s2 += recurse_def((v.last.is_a?(Array) ? v.last : [v.last]), import_columns, assocs, joins_array, prefixes, v.first.to_sym).first
|
500
|
-
end
|
501
|
-
elsif col.nil?
|
502
|
-
if assocs.empty?
|
503
|
-
[]
|
504
|
-
else
|
505
|
-
# Bring in from another class
|
506
|
-
joins << { prefix => (joins_array = []) }
|
507
|
-
# %%% Also bring in uniques and requireds
|
508
|
-
recurse_def(assocs.last.klass::IMPORT_COLUMNS[:all], import_columns, assocs, joins_array, prefixes).first
|
509
|
-
end
|
510
|
-
else
|
511
|
-
[::DutyFree::Column.new(col, pre_prefix, prefix, assocs, self, import_columns[:as])]
|
512
|
-
end
|
513
|
-
s
|
514
|
-
end
|
515
|
-
[array, joins]
|
660
|
+
# The snake-cased column alias names used in the query to export data
|
661
|
+
def self._template_columns(klass, import_template = nil)
|
662
|
+
template_detail_columns = klass.instance_variable_get(:@template_detail_columns)
|
663
|
+
if klass.instance_variable_get(:@template_import_columns) != import_template
|
664
|
+
klass.instance_variable_set(:@template_import_columns, import_template)
|
665
|
+
klass.instance_variable_set(:@template_detail_columns, (template_detail_columns = nil))
|
516
666
|
end
|
517
|
-
|
667
|
+
unless template_detail_columns
|
668
|
+
# puts "* Redoing *"
|
669
|
+
template_detail_columns = _recurse_def(klass, import_template[:all], import_template).first.map(&:to_sym)
|
670
|
+
klass.instance_variable_set(:@template_detail_columns, template_detail_columns)
|
671
|
+
end
|
672
|
+
template_detail_columns
|
673
|
+
end
|
674
|
+
|
675
|
+
# Recurse and return two arrays -- one with all columns in sequence, and one a hierarchy of
|
676
|
+
# nested hashes to be used with ActiveRecord's .joins() to facilitate export.
|
677
|
+
def self._recurse_def(klass, array, import_template, assocs = [], joins = [], pre_prefix = '', prefix = '')
|
678
|
+
# Confirm we can actually navigate through this association
|
679
|
+
prefix_assoc = (assocs.last&.klass || klass).reflect_on_association(prefix) if prefix.present?
|
680
|
+
assocs = assocs.dup << prefix_assoc unless prefix_assoc.nil?
|
681
|
+
prefixes = ::DutyFree::Util._prefix_join([pre_prefix, prefix])
|
682
|
+
array = array.inject([]) do |s, col|
|
683
|
+
s + if col.is_a?(Hash)
|
684
|
+
col.inject([]) do |s2, v|
|
685
|
+
joins << { v.first.to_sym => (joins_array = []) }
|
686
|
+
s2 + _recurse_def(klass, (v.last.is_a?(Array) ? v.last : [v.last]), import_template, assocs, joins_array, prefixes, v.first.to_sym).first
|
687
|
+
end
|
688
|
+
elsif col.nil?
|
689
|
+
if assocs.empty?
|
690
|
+
[]
|
691
|
+
else
|
692
|
+
# Bring in from another class
|
693
|
+
joins << { prefix => (joins_array = []) }
|
694
|
+
# %%% Also bring in uniques and requireds
|
695
|
+
_recurse_def(klass, assocs.last.klass::IMPORT_TEMPLATE[:all], import_template, assocs, joins_array, prefixes).first
|
696
|
+
end
|
697
|
+
else
|
698
|
+
[::DutyFree::Column.new(col, pre_prefix, prefix, assocs, klass, import_template[:as])]
|
699
|
+
end
|
700
|
+
end
|
701
|
+
[array, joins]
|
702
|
+
end
|
518
703
|
end # module Extensions
|
519
704
|
|
520
|
-
|
705
|
+
# Rails < 4.0 doesn't have ActiveRecord::RecordNotUnique, so use the more generic ActiveRecord::ActiveRecordError instead
|
706
|
+
ar_not_unique_error = ActiveRecord.const_defined?('RecordNotUnique') ? ActiveRecord::RecordNotUnique : ActiveRecord::ActiveRecordError
|
707
|
+
class NoUniqueColumnError < ar_not_unique_error
|
521
708
|
end
|
522
709
|
|
523
|
-
|
710
|
+
# Rails < 4.2 doesn't have ActiveRecord::RecordInvalid, so use the more generic ActiveRecord::ActiveRecordError instead
|
711
|
+
ar_invalid_error = ActiveRecord.const_defined?('RecordInvalid') ? ActiveRecord::RecordInvalid : ActiveRecord::ActiveRecordError
|
712
|
+
class LessThanHalfAreMatchingColumnsError < ar_invalid_error
|
524
713
|
end
|
525
714
|
end
|