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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c9a4a9d552788b5fa4d273e4c000d8d12bbd7f8808eb7904c69bc57c7ccbd61
4
- data.tar.gz: 3e7ccb0be3785814ef2c2bcb488b73d52a956c9ebcc6365ab9a0e3a4160a3924
3
+ metadata.gz: 51bb54397528578ae7d4d43a04d1d6c31f81d1417d9c076f1fb751b18c09afaa
4
+ data.tar.gz: 8561d19c0a0166462c011f6a310429944de9b95751dbf9b36dbbb32fdce4bf76
5
5
  SHA512:
6
- metadata.gz: 10df4b7fbf2d78d2f4c9d24ea318461465e61064697b315e5f84ed92e2d342347a17d0c07e2440858f19453fe0a128524ba70b1adb7c7a564ed84d786ffca7b1
7
- data.tar.gz: 1e377aa74a39e3fe9b3f4accda7ef83535bf3ca2b0fe36de1b3b50289df30a96192d6eca22265e4d6749cf2101b4316cc29211fb450aeb0c2112f84b64f031d8
6
+ metadata.gz: 02ae8681512885e5ab618aaef343ae88375338fc396ec711b28db78bd97daad361949b86c18c1ca81401e3a15f6629814900a751145bf8a270e50f1fa5669cab
7
+ data.tar.gz: 0444426a5468f3dcf7143f3f8859f79f2808db968b6a59561ff0351223993b0071aa94e95b4e50924d99ccbf4ae78f7a9b190a617eac6c21305d8ae0e7dd7f87
@@ -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
 
@@ -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, :import_columns_as
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, import_columns_as)
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.import_columns_as = import_columns_as
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
- sql_col = ::DutyFree::Util._prefix_join([mapping["#{pre_prefix.tr('.', '_')}_#{prefix}_"], name])
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 ||= sym_string.titleize
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 sym_string
47
+ def to_sym
50
48
  @sym_string ||= ::DutyFree::Util._prefix_join(
51
- [pre_prefix, prefix, ::DutyFree::Util._clean_name(name, import_columns_as)],
49
+ [pre_prefix, prefix, ::DutyFree::Util._clean_name(name, import_template_as)],
52
50
  '_'
53
51
  ).tr('.', '_')
54
52
  end
@@ -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, import_columns = 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)
25
24
  # In case they are only supplying the columns hash
26
- if is_with_data.is_a?(Hash) && !import_columns
27
- import_columns = is_with_data
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
- import_columns ||= if constants.include?(:IMPORT_COLUMNS)
31
- self::IMPORT_COLUMNS
32
- else
33
- suggest_template(0, false, false)
34
- end
35
- rows = [friendly_columns(import_columns)]
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 = recurse_def(import_columns[:all], import_columns)
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
- # Warning: Delegating ast to arel is deprecated and will be removed in Rails 6.0
44
- arel_alias_names = ::DutyFree::Util._recurse_arel(relation.ast.cores.first.source)
45
- our_names = ::DutyFree::Util._recurse_arel(template_joins)
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 << template_columns(import_columns).map do |col|
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, import_columns = nil)
67
- import_columns ||= if constants.include?(:IMPORT_COLUMNS)
68
- self::IMPORT_COLUMNS
69
- else
70
- suggest_template(0, false, false)
71
- end
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 = import_columns[: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
- end
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
- data = if data.length <= 4096 && data.split('\n').length == 1
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
- # Hope that other multi-line strings might be CSV data
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 before_import ||= (import_columns[:before_import]) # || some generic before_import)
127
- before_import.call(data)
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 = import_columns[: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
- defined_uniques(uniques, cols, starred)
158
- cols.map! { |col| ::DutyFree::Util._clean_name(col, import_columns[:as]) } # %%%
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 = recurse_def(import_columns[:all], import_columns).first
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 = valid_uniques(uniques, cols, starred, import_columns)
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) { |v, s| s[v[1..-1].map(&:to_s)] = v.first; }
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 << row[v.last].to_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 ||= import_columns[: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
- if sub_obj&.valid?
204
- # %%% Perhaps send them even invalid objects so they can be made valid here?
205
- if around_import_save
206
- around_import_save(sub_obj) do |yes_do_save|
207
- sub_obj.save if yes_do_save && sub_obj&.valid?
208
- end
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
- sub_next = if is_has_one
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
- trim_prefix = v.titleize[0..-(v.name.length + 2)]
291
+ sub_bt = nil
244
292
  begin
245
- sub_unique = assoc.klass.valid_uniques(uniques, cols, starred, import_columns, all, trim_prefix)
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
- # Find by all corresponding columns
250
- criteria = sub_unique&.inject({}) do |s, v|
251
- s[v.first.to_sym] = row[v.last]
252
- s
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
- if sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
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 IMPORT_COLUMNS[:all] that this will break. Need to test deeply nested things.
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
- puts sub_next.klass
273
- sub_unique = sub_next.klass.valid_uniques(uniques, cols, starred, import_columns, all, trim_prefix)
274
- # Find by all corresponding columns
275
- criteria = sub_unique.each_with_object({}) { |v, s| s[v.first.to_sym] = row[v.last]; }
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 unless sub_next.nil?
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 = sub_class.columns_hash[v.name.to_s]&.type
312
- if col_type.nil? && (virtual_columns = import_columns[: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 'true', 't', 'on'
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 'false', 'f', 'off'
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 " #{sub_class.name} doesn't respond to #{sym}"
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 opportinity to tweak user objects controlled by Devise
351
- is_do_save = before_devise_save(obj, existing) if before_devise_save && obj.class.name == devise_class
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.inject([]) do |s, v|
369
- s + v.last[1..-1].map { |line_num| { line_num => v.first } } if v.last.count > 1
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
- if @after_import ||= (import_columns[:after_import]) # || some generic after_import)
374
- ret = ret2 if (ret2 = @after_import.call(ret)).is_a?(Hash)
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
- # Friendly column names that end up in the first row of the CSV
381
- # Required columns get prefixed with a *
382
- def friendly_columns(import_columns = self::IMPORT_COLUMNS)
383
- requireds = (import_columns[:required] || [])
384
- template_columns(import_columns).map do |col|
385
- is_required = requireds.include?(col)
386
- col = col.to_s.titleize
387
- # Alias-ify the full column names
388
- aliases = (import_columns[:as] || [])
389
- aliases.each do |k, v|
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
- end
440
+ col_list = cols.join('|')
398
441
 
399
- # The snake-cased column alias names used in the query to export data
400
- def template_columns(import_columns = nil)
401
- if @template_import_columns != import_columns
402
- @template_import_columns = import_columns
403
- @template_detail_columns = nil
404
- end
405
- @template_detail_columns ||= recurse_def(import_columns[:all], import_columns).first.map(&:to_sym)
406
- end
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
- # For use with importing, based on the provided column list calculate all valid combinations
409
- # of unique columns. If there is no valid combination, throws an error.
410
- def valid_uniques(uniques, cols, starred, import_columns, all = nil, trim_prefix = '')
411
- col_name_offset = (trim_prefix.blank? ? 0 : trim_prefix.length + 1)
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 = recurse_def(all || import_columns[:all], import_columns).first
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
- trim_prefix_snake == ::DutyFree::Util._prefix_join([col.pre_prefix, col.prefix], '_').tr('.', '_')
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
- vus = defined_uniques(uniques, cols, starred).select do |k, _v|
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
- k.each do |k_col|
429
- unless k_col.start_with?(trim_prefix) && available.include?(k_col[col_name_offset..-1])
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
- ret
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
- @defined_uniques ||= {}
457
- unless (defined_uniques = @defined_uniques[cols])
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
- defined_uniques = uniques.each_with_object({}) do |unique, s|
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 = cols.index(unique_part_name = unique_part.to_s.titleize)
465
- next if val.nil?
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 << unique_part_name
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 = cols.index(unique_part_name = unique.to_s.titleize)
476
- unless val.nil?
477
- s[[unique_part_name]] = [val]
478
- utilised[[unique_part_name]] = nil
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
- (starred - utilised.keys).each { |star| defined_uniques[[star]] = [cols.index(star)] }
483
- @defined_uniques[cols] = defined_uniques
654
+ @defined_uniques[col_list] = defined_uniq
484
655
  end
485
- defined_uniques
656
+ defined_uniq
486
657
  end
658
+ end # module ClassMethods
487
659
 
488
- # Recurse and return two arrays -- one with all columns in sequence, and one a hierarchy of
489
- # nested hashes to be used with ActiveRecord's .joins() to facilitate export.
490
- def recurse_def(array, import_columns, assocs = [], joins = [], pre_prefix = '', prefix = '')
491
- # Confirm we can actually navigate through this association
492
- prefix_assoc = (assocs.last&.klass || self).reflect_on_association(prefix) if prefix.present?
493
- assocs = assocs.dup << prefix_assoc unless prefix_assoc.nil?
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
- end # module ClassMethods
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
- class NoUniqueColumnError < ActiveRecord::RecordNotUnique
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
- class LessThanHalfAreMatchingColumnsError < ActiveRecord::RecordInvalid
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