duty_free 1.0.4 → 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 +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
|
- - ">="
|