duty_free 1.0.0 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
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