duty_free 1.0.4 → 1.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/duty_free.rb +130 -0
- data/lib/duty_free/column.rb +2 -6
- data/lib/duty_free/extensions.rb +157 -109
- data/lib/duty_free/suggest_template.rb +8 -4
- data/lib/duty_free/util.rb +22 -8
- data/lib/duty_free/version_number.rb +1 -1
- metadata +10 -11
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
@@ -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
@@ -19,7 +19,8 @@ module DutyFree
|
|
19
19
|
# end
|
20
20
|
|
21
21
|
# Export at least column header, and optionally include all existing data as well
|
22
|
-
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)
|
23
24
|
# In case they are only supplying the columns hash
|
24
25
|
if is_with_data.is_a?(Hash) && !import_template
|
25
26
|
import_template = is_with_data
|
@@ -52,12 +53,20 @@ module DutyFree
|
|
52
53
|
if is_with_data
|
53
54
|
# Automatically create a JOINs strategy and select list to get back all related rows
|
54
55
|
template_cols, template_joins = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template)
|
55
|
-
relation = left_joins(template_joins)
|
56
|
+
relation = use_inner_joins ? joins(template_joins) : left_joins(template_joins)
|
56
57
|
|
57
58
|
# So we can properly create the SELECT list, create a mapping between our
|
58
59
|
# column alias prefixes and the aliases AREL creates.
|
59
|
-
|
60
|
-
|
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)
|
61
70
|
mapping = our_names.zip(arel_alias_names).to_h
|
62
71
|
|
63
72
|
relation.select(template_cols.map { |x| x.to_s(mapping) }).each do |result|
|
@@ -157,7 +166,6 @@ module DutyFree
|
|
157
166
|
arguments = [data, import_template][0..last_arg_idx]
|
158
167
|
data = ret if (ret = my_before_import.call(*arguments)).is_a?(Enumerable)
|
159
168
|
end
|
160
|
-
col_list = nil
|
161
169
|
data.each_with_index do |row, row_num|
|
162
170
|
row_errors = {}
|
163
171
|
if is_first # Anticipate that first row has column names
|
@@ -186,10 +194,7 @@ module DutyFree
|
|
186
194
|
col.strip!
|
187
195
|
end
|
188
196
|
end
|
189
|
-
|
190
|
-
# after the next line, the map! with clean to change out the alias names? So we can't yet set
|
191
|
-
# col_list?
|
192
|
-
cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) } # %%%
|
197
|
+
cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) }
|
193
198
|
defined_uniques(uniques, cols, cols.join('|'), starred)
|
194
199
|
# Make sure that at least half of them match what we know as being good column names
|
195
200
|
template_column_objects = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template).first
|
@@ -210,9 +215,9 @@ module DutyFree
|
|
210
215
|
is_first = false
|
211
216
|
else # Normal row of data
|
212
217
|
is_insert = false
|
213
|
-
is_do_save = true
|
214
218
|
existing_unique = valid_unique.inject([]) do |s, v|
|
215
219
|
s << if v.last.is_a?(Array)
|
220
|
+
# binding.pry
|
216
221
|
v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck(MAX_ID).first.to_s
|
217
222
|
else
|
218
223
|
row[v.last].to_s
|
@@ -252,6 +257,7 @@ module DutyFree
|
|
252
257
|
end
|
253
258
|
sub_obj = obj
|
254
259
|
this_path = +''
|
260
|
+
# puts "p: #{v.path}"
|
255
261
|
v.path.each_with_index do |path_part, idx|
|
256
262
|
this_path << (this_path.blank? ? path_part.to_s : ",#{path_part}")
|
257
263
|
unless (sub_next = sub_objects[this_path])
|
@@ -274,56 +280,45 @@ module DutyFree
|
|
274
280
|
# This works for belongs_to or has_one. has_many gets sorted below.
|
275
281
|
# Get existing related object, or create a new one
|
276
282
|
if (sub_next = sub_obj.send(path_part)).nil?
|
277
|
-
is_has_one = assoc.is_a?(ActiveRecord::Reflection::HasOneReflection)
|
278
283
|
klass = Object.const_get(assoc&.class_name)
|
279
|
-
|
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
|
280
287
|
has_ones << v.path
|
281
288
|
klass.new
|
282
289
|
else
|
283
290
|
# Try to find a unique item if one is referenced
|
284
291
|
sub_bt = nil
|
285
292
|
begin
|
286
|
-
# Goofs up if trim_prefix isn't the same name as the class, or if it's
|
287
|
-
# a self-join? (like when trim_prefix == 'Reports To')
|
288
|
-
# %%% Need to test this more when the self-join is more than one hop away,
|
289
|
-
# such as importing orders and having employees come along :)
|
290
|
-
# if sub_obj.class == klass
|
291
|
-
# trim_prefix = ''
|
292
|
-
# end
|
293
|
-
# %%% Maybe instead of passing in "klass" we can give the belongs_to association and build through that instead,
|
294
|
-
# allowing us to nix the klass.new(criteria) line below.
|
295
293
|
trim_prefix = v.titleize[0..-(v.name.length + 2)]
|
296
294
|
trim_prefix << ' ' unless trim_prefix.blank?
|
297
|
-
# if klass == sub_obj.class # Self-referencing thing pointing to us?
|
298
|
-
# %%% This should be more general than just for self-referencing things.
|
299
295
|
sub_bt, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, nil, row, klass, all, trim_prefix)
|
300
296
|
rescue ::DutyFree::NoUniqueColumnError
|
301
|
-
sub_unique = nil
|
302
297
|
end
|
303
|
-
#
|
304
|
-
# %%% Can criteria really ever be empty anymore?
|
298
|
+
# %%% Can criteria really ever be nil anymore?
|
305
299
|
sub_bt ||= klass.new(criteria || {}) unless klass == sub_obj.class && criteria.empty?
|
306
300
|
sub_obj.send("#{path_part}=", sub_bt)
|
307
301
|
sub_bt
|
308
302
|
end
|
309
303
|
end
|
310
304
|
# Look for possible missing polymorphic detail
|
305
|
+
# Maybe can test for this via assoc.through_reflection
|
311
306
|
if assoc.is_a?(ActiveRecord::Reflection::ThroughReflection) &&
|
312
307
|
(delegate = assoc.send(:delegate_reflection)&.active_record&.reflect_on_association(assoc.source_reflection_name)) &&
|
313
308
|
delegate.options[:polymorphic]
|
314
309
|
polymorphics << { parent: sub_next, child: sub_obj, type_col: delegate.foreign_type, id_col: delegate.foreign_key.to_s }
|
315
310
|
end
|
316
311
|
# From a has_many?
|
317
|
-
|
312
|
+
# Rails 4.0 and later can do: sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
|
313
|
+
if assoc.macro == :has_many && !assoc.options[:through]
|
318
314
|
# Try to find a unique item if one is referenced
|
319
315
|
# %%% There is possibility that when bringing in related classes using a nil
|
320
316
|
# in IMPORT_TEMPLATE[:all] that this will break. Need to test deeply nested things.
|
321
317
|
start = (v.pre_prefix.blank? ? 0 : v.pre_prefix.length)
|
322
318
|
trim_prefix = v.titleize[start..-(v.name.length + 2)]
|
323
319
|
trim_prefix << ' ' unless trim_prefix.blank?
|
324
|
-
klass = sub_next.klass
|
325
320
|
# assoc.inverse_of is the belongs_to side of the has_many train we came in here on.
|
326
|
-
sub_hm, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
|
321
|
+
sub_hm, criteria = assoc.klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
|
327
322
|
|
328
323
|
# If still not found then create a new related object using this has_many collection
|
329
324
|
# (criteria.empty? ? nil : sub_next.new(criteria))
|
@@ -347,7 +342,7 @@ module DutyFree
|
|
347
342
|
|
348
343
|
next unless sub_obj.respond_to?(sym = "#{v.name}=".to_sym)
|
349
344
|
|
350
|
-
col_type =
|
345
|
+
col_type = sub_obj.class.columns_hash[v.name.to_s]&.type
|
351
346
|
if col_type.nil? && (virtual_columns = import_template[:virtual_columns]) &&
|
352
347
|
(virtual_columns = virtual_columns[this_path] || virtual_columns)
|
353
348
|
col_type = virtual_columns[v.name]
|
@@ -355,9 +350,9 @@ module DutyFree
|
|
355
350
|
if col_type == :boolean
|
356
351
|
if row[key].nil?
|
357
352
|
# Do nothing when it's nil
|
358
|
-
elsif %w[true t yes y].include?(row[key]&.downcase) # Used to cover 'on'
|
353
|
+
elsif %w[true t yes y].include?(row[key]&.strip&.downcase) # Used to cover 'on'
|
359
354
|
row[key] = true
|
360
|
-
elsif %w[false f no n].include?(row[key]&.downcase) # Used to cover 'off'
|
355
|
+
elsif %w[false f no n].include?(row[key]&.strip&.downcase) # Used to cover 'off'
|
361
356
|
row[key] = false
|
362
357
|
else
|
363
358
|
row_errors[v.name] ||= []
|
@@ -366,7 +361,7 @@ module DutyFree
|
|
366
361
|
end
|
367
362
|
sub_obj.send(sym, row[key])
|
368
363
|
# else
|
369
|
-
# puts " #{
|
364
|
+
# puts " #{sub_obj.class.name} doesn't respond to #{sym}"
|
370
365
|
end
|
371
366
|
# Try to save a final sub-object if one exists
|
372
367
|
sub_obj.save if sub_obj && this_path && !is_has_one && sub_obj.valid?
|
@@ -442,31 +437,54 @@ module DutyFree
|
|
442
437
|
s
|
443
438
|
end
|
444
439
|
end
|
440
|
+
col_list = cols.join('|')
|
445
441
|
|
446
442
|
# First add in foreign key stuff we can find from belongs_to associations (other than the
|
447
443
|
# one we might have arrived here upon).
|
448
|
-
criteria = {}
|
444
|
+
criteria = {} # Enough detail to find or build a new object
|
449
445
|
bt_criteria = {}
|
450
446
|
bt_criteria_all_nil = true
|
451
447
|
bt_col_indexes = []
|
452
448
|
available_bts = []
|
453
449
|
only_valid_uniques = (train_we_came_in_here_on == false)
|
454
|
-
uniq_lookups = {}
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
450
|
+
uniq_lookups = {} # The data, or how to look up the data
|
451
|
+
|
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
|
456
|
+
# Find all unique combinations that are available based on incoming columns, and
|
457
|
+
# pair them up with column number mappings.
|
458
|
+
template_column_objects = ::DutyFree::Extensions._recurse_def(self, all || import_template[:all], import_template).first
|
459
|
+
available = if trim_prefix.blank?
|
460
|
+
template_column_objects.select { |col| col.pre_prefix.blank? && col.prefix.blank? }
|
461
|
+
else
|
462
|
+
trim_prefix_snake = trim_prefix.downcase.tr(' ', '_')
|
463
|
+
template_column_objects.select do |col|
|
464
|
+
this_prefix = ::DutyFree::Util._prefix_join([col.pre_prefix, col.prefix], '_').tr('.', '_')
|
465
|
+
trim_prefix_snake == "#{this_prefix}_"
|
466
|
+
end
|
467
|
+
end.map { |avail| avail.name.to_s.titleize }
|
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)
|
464
475
|
end
|
465
|
-
|
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
|
+
|
466
484
|
# This search prefix becomes something like "Order Details Product "
|
467
485
|
cols.each_with_index do |bt_col, idx|
|
468
486
|
next if bt_col_indexes.include?(idx) ||
|
469
|
-
!bt_col&.start_with?(trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} ")
|
487
|
+
!bt_col&.start_with?(bt_prefix = (trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} "))
|
470
488
|
|
471
489
|
available_bts << bt_col
|
472
490
|
fk_id = if row
|
@@ -475,6 +493,26 @@ module DutyFree
|
|
475
493
|
# (like first_name, last_name on a referenced employee)
|
476
494
|
sn_bt.klass.where(keepers[idx].name => row[idx]).limit(1).pluck(MAX_ID).first
|
477
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
|
478
516
|
[sn_bt.klass, keepers[idx].name, idx]
|
479
517
|
end
|
480
518
|
if fk_id
|
@@ -482,74 +520,76 @@ module DutyFree
|
|
482
520
|
bt_criteria_all_nil = false
|
483
521
|
end
|
484
522
|
bt_criteria[(fk_name = sn_bt.foreign_key)] = fk_id
|
523
|
+
|
485
524
|
# Add to our criteria if this belongs_to is required
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
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
|
493
536
|
end
|
494
537
|
end
|
495
538
|
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
trim_prefix_snake = trim_prefix.downcase.tr(' ', '_')
|
506
|
-
template_column_objects.select do |col|
|
507
|
-
this_prefix = ::DutyFree::Util._prefix_join([col.pre_prefix, col.prefix], '_').tr('.', '_')
|
508
|
-
trim_prefix_snake == "#{this_prefix}_"
|
509
|
-
end
|
510
|
-
end.map { |avail| avail.name.to_s.titleize }
|
511
|
-
vus = defined_uniques(uniques, cols, nil, starred).select do |k, _v|
|
512
|
-
is_good = true
|
513
|
-
k.each do |k_col|
|
514
|
-
unless available.include?(k_col) || available_bts.include?(k_col)
|
515
|
-
is_good = false
|
516
|
-
break
|
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]
|
517
548
|
end
|
518
549
|
end
|
519
|
-
|
550
|
+
vus[combined_k] = combined_v unless combined_k.empty?
|
520
551
|
end
|
521
|
-
@valid_uniques[col_list] = vus
|
522
552
|
end
|
523
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
|
+
|
524
564
|
# Make sure they have at least one unique combination to take cues from
|
525
565
|
unless vus.empty? # raise NoUniqueColumnError.new(I18n.t('import.no_unique_column_error'))
|
526
566
|
# Convert the first entry to a simplified hash, such as:
|
527
567
|
# {[:investigator_institutions_name, :investigator_institutions_email] => [8, 9], ...}
|
528
568
|
# to {:name => 8, :email => 9}
|
529
|
-
key, val = vus.first
|
569
|
+
key, val = vus.first # Utilise the first identified set of valid uniques
|
530
570
|
key.each_with_index do |k, idx|
|
531
571
|
next if available_bts.include?(k) # These will be provided in criteria, and not uniq_lookups
|
532
572
|
|
533
573
|
# uniq_lookups[k[trim_prefix.length..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
# Find by all corresponding columns
|
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
|
539
577
|
|
540
|
-
|
541
|
-
unless only_valid_uniques
|
542
|
-
uniq_lookups.each do |k, v|
|
543
|
-
next if bt_col_indexes.include?(v)
|
578
|
+
next if only_valid_uniques || bt_col_indexes.include?(v)
|
544
579
|
|
580
|
+
# Find by all corresponding columns
|
545
581
|
if (row_value = row[v])
|
546
582
|
new_criteria_all_nil = false
|
547
|
-
criteria[
|
583
|
+
criteria[k_sym] = row_value # The data, or how to look up the data
|
548
584
|
end
|
549
585
|
end
|
550
586
|
end
|
551
587
|
|
552
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
|
553
593
|
return uniq_lookups.merge(criteria) if only_valid_uniques
|
554
594
|
|
555
595
|
# If there's nothing to match upon then we're out
|
@@ -560,35 +600,37 @@ module DutyFree
|
|
560
600
|
found_object = klass_or_collection.find_by(criteria)
|
561
601
|
# If not successful, such as when fields are exposed via helper methods instead of being
|
562
602
|
# real columns in the database tables, try this more intensive routine.
|
563
|
-
found_object
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
603
|
+
unless found_object || klass_or_collection.is_a?(Array)
|
604
|
+
found_object = klass_or_collection.find do |obj|
|
605
|
+
is_good = true
|
606
|
+
criteria.each do |k, v|
|
607
|
+
if obj.send(k).to_s != v.to_s
|
608
|
+
is_good = false
|
609
|
+
break
|
610
|
+
end
|
569
611
|
end
|
612
|
+
is_good
|
570
613
|
end
|
571
|
-
is_good
|
572
614
|
end
|
573
615
|
[found_object, criteria.merge(bt_criteria)]
|
574
616
|
end
|
575
617
|
|
576
618
|
private
|
577
619
|
|
578
|
-
def defined_uniques(uniques, cols = [], col_list = nil, starred = [])
|
620
|
+
def defined_uniques(uniques, cols = [], col_list = nil, starred = [], trim_prefix = '')
|
579
621
|
col_list ||= cols.join('|')
|
580
|
-
@defined_uniques ||= {}
|
581
|
-
unless (defined_uniq = @defined_uniques[col_list])
|
622
|
+
unless (defined_uniq = (@defined_uniques ||= {})[col_list])
|
582
623
|
utilised = {} # Track columns that have been referenced thusfar
|
583
624
|
defined_uniq = uniques.each_with_object({}) do |unique, s|
|
584
625
|
if unique.is_a?(Array)
|
585
626
|
key = []
|
586
627
|
value = []
|
587
628
|
unique.each do |unique_part|
|
588
|
-
val =
|
589
|
-
|
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
|
590
632
|
|
591
|
-
key <<
|
633
|
+
key << upn
|
592
634
|
value << val
|
593
635
|
end
|
594
636
|
unless key.empty?
|
@@ -596,12 +638,14 @@ module DutyFree
|
|
596
638
|
utilised[key] = nil
|
597
639
|
end
|
598
640
|
else
|
599
|
-
val =
|
600
|
-
|
601
|
-
|
602
|
-
|
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
|
603
646
|
end
|
604
647
|
end
|
648
|
+
s
|
605
649
|
end
|
606
650
|
if defined_uniq.empty?
|
607
651
|
(starred - utilised.keys).each { |star| defined_uniq[[star]] = [cols.index(star)] }
|
@@ -616,8 +660,8 @@ module DutyFree
|
|
616
660
|
# The snake-cased column alias names used in the query to export data
|
617
661
|
def self._template_columns(klass, import_template = nil)
|
618
662
|
template_detail_columns = klass.instance_variable_get(:@template_detail_columns)
|
619
|
-
if
|
620
|
-
klass.instance_variable_set(:@template_import_columns,
|
663
|
+
if klass.instance_variable_get(:@template_import_columns) != import_template
|
664
|
+
klass.instance_variable_set(:@template_import_columns, import_template)
|
621
665
|
klass.instance_variable_set(:@template_detail_columns, (template_detail_columns = nil))
|
622
666
|
end
|
623
667
|
unless template_detail_columns
|
@@ -639,7 +683,7 @@ module DutyFree
|
|
639
683
|
s + if col.is_a?(Hash)
|
640
684
|
col.inject([]) do |s2, v|
|
641
685
|
joins << { v.first.to_sym => (joins_array = []) }
|
642
|
-
s2
|
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
|
643
687
|
end
|
644
688
|
elsif col.nil?
|
645
689
|
if assocs.empty?
|
@@ -658,9 +702,13 @@ module DutyFree
|
|
658
702
|
end
|
659
703
|
end # module Extensions
|
660
704
|
|
661
|
-
|
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
|
662
708
|
end
|
663
709
|
|
664
|
-
|
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
|
665
713
|
end
|
666
714
|
end
|
@@ -40,15 +40,18 @@ module DutyFree
|
|
40
40
|
assocs = {}
|
41
41
|
this_klass.reflect_on_all_associations.each do |assoc|
|
42
42
|
# PolymorphicReflection AggregateReflection RuntimeReflection
|
43
|
-
is_belongs_to = assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection)
|
43
|
+
is_belongs_to = assoc.belongs_to? # is_a?(ActiveRecord::Reflection::BelongsToReflection)
|
44
44
|
# Figure out if it's belongs_to, has_many, or has_one
|
45
|
+
# HasAndBelongsToManyReflection
|
45
46
|
belongs_to_or_has_many =
|
46
47
|
if is_belongs_to
|
47
48
|
'belongs_to'
|
48
|
-
elsif (is_habtm = assoc.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection))
|
49
|
+
elsif (is_habtm = assoc.respond_to?(:macro) ? (assoc.macro == :has_and_belongs_to_many) : assoc.is_a?(ActiveRecord::Reflection::HasAndBelongsToManyReflection))
|
49
50
|
'has_and_belongs_to_many'
|
51
|
+
elsif assoc.respond_to?(:macro) ? (assoc.macro == :has_many) : assoc.is_a?(ActiveRecord::Reflection::HasManyReflection)
|
52
|
+
'has_many'
|
50
53
|
else
|
51
|
-
|
54
|
+
'has_one'
|
52
55
|
end
|
53
56
|
# Always process belongs_to, and also process has_one and has_many if do_has_many is chosen.
|
54
57
|
# Skip any HMT or HABTM. (Maybe break out HABTM into a combo HM and BT in the future.)
|
@@ -178,7 +181,8 @@ module DutyFree
|
|
178
181
|
# Find belongs_tos for this model to one more more other klasses
|
179
182
|
def self._find_belongs_tos(klass, to_klass, errored_assocs)
|
180
183
|
klass.reflect_on_all_associations.each_with_object([]) do |bt_assoc, s|
|
181
|
-
|
184
|
+
# .is_a?(ActiveRecord::Reflection::BelongsToReflection)
|
185
|
+
next unless bt_assoc.belongs_to? && !errored_assocs.include?(bt_assoc)
|
182
186
|
|
183
187
|
begin
|
184
188
|
s << bt_assoc if !bt_assoc.polymorphic? && bt_assoc.klass == to_klass
|
data/lib/duty_free/util.rb
CHANGED
@@ -35,20 +35,34 @@ module DutyFree
|
|
35
35
|
end
|
36
36
|
|
37
37
|
# ActiveRecord AREL objects
|
38
|
-
elsif piece.is_a?(Arel::Nodes::JoinSource)
|
39
|
-
# The left side is the "FROM" table
|
40
|
-
# names += _recurse_arel(piece.left)
|
41
|
-
# The right side is an array of all JOINs
|
42
|
-
names += piece.right.inject([]) { |s, v| s + _recurse_arel(v) }
|
43
38
|
elsif piece.is_a?(Arel::Nodes::Join) # INNER or OUTER JOIN
|
44
|
-
#
|
45
|
-
|
46
|
-
|
39
|
+
# rubocop:disable Style/IdenticalConditionalBranches
|
40
|
+
if piece.right.is_a?(Arel::Table) # Came in from AR < 3.2?
|
41
|
+
# Arel 2.x and older is a little curious because these JOINs work "back to front".
|
42
|
+
# The left side here is either another earlier JOIN, or at the end of the whole tree, it is
|
43
|
+
# the first table.
|
44
|
+
names += _recurse_arel(piece.left)
|
45
|
+
# The right side here at the top is the very last table, and anywhere else down the tree it is
|
46
|
+
# the later "JOIN" table of this pair. (The table that comes after all the rest of the JOINs
|
47
|
+
# from the left side.)
|
48
|
+
names << piece.right.name
|
49
|
+
else # "Normal" setup, fed from a JoinSource which has an array of JOINs
|
50
|
+
# The left side is the "JOIN" table
|
51
|
+
names += _recurse_arel(piece.left)
|
52
|
+
# (The right side of these is the "ON" clause)
|
53
|
+
end
|
54
|
+
# rubocop:enable Style/IdenticalConditionalBranches
|
47
55
|
elsif piece.is_a?(Arel::Table) # Table
|
48
56
|
names << piece.name
|
49
57
|
elsif piece.is_a?(Arel::Nodes::TableAlias) # Alias
|
50
58
|
# Can get the real table name from: self._recurse_arel(piece.left)
|
51
59
|
names << piece.right.to_s # This is simply a string; the alias name itself
|
60
|
+
elsif piece.is_a?(Arel::Nodes::JoinSource) # Leaving this until the end because AR < 3.2 doesn't know at all about JoinSource!
|
61
|
+
# The left side is the "FROM" table
|
62
|
+
# names += _recurse_arel(piece.left)
|
63
|
+
names << piece.left.name
|
64
|
+
# The right side is an array of all JOINs
|
65
|
+
names += piece.right.inject([]) { |s, v| s + _recurse_arel(v) }
|
52
66
|
end
|
53
67
|
names
|
54
68
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: duty_free
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lorin Thwaits
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-11-
|
11
|
+
date: 2020-11-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,20 +16,20 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '3.0'
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '6.
|
22
|
+
version: '6.1'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
26
26
|
requirements:
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: '
|
29
|
+
version: '3.0'
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '6.
|
32
|
+
version: '6.1'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: appraisal
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -204,10 +204,9 @@ dependencies:
|
|
204
204
|
- - "~>"
|
205
205
|
- !ruby/object:Gem::Version
|
206
206
|
version: '1.4'
|
207
|
-
description:
|
208
|
-
|
209
|
-
|
210
|
-
XLSX, ODT, HTML tables, or simple Ruby arrays.
|
207
|
+
description: 'Simplify data imports and exports with this slick ActiveRecord extension
|
208
|
+
|
209
|
+
'
|
211
210
|
email: lorint@gmail.com
|
212
211
|
executables: []
|
213
212
|
extensions: []
|
@@ -243,7 +242,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
243
242
|
requirements:
|
244
243
|
- - ">="
|
245
244
|
- !ruby/object:Gem::Version
|
246
|
-
version: 2.
|
245
|
+
version: 2.3.5
|
247
246
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
248
247
|
requirements:
|
249
248
|
- - ">="
|