duty_free 1.0.0 → 1.0.1

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: 87d07568c8c7a1692981435e098bd8426d17f30cd733ffff01a7b5a4e4fee4a9
4
+ data.tar.gz: 1d2ef595a271f40e5489cac67e576b4f85338d43d46bff26c6e5a6d706766a00
5
5
  SHA512:
6
- metadata.gz: 10df4b7fbf2d78d2f4c9d24ea318461465e61064697b315e5f84ed92e2d342347a17d0c07e2440858f19453fe0a128524ba70b1adb7c7a564ed84d786ffca7b1
7
- data.tar.gz: 1e377aa74a39e3fe9b3f4accda7ef83535bf3ca2b0fe36de1b3b50289df30a96192d6eca22265e4d6749cf2101b4316cc29211fb450aeb0c2112f84b64f031d8
6
+ metadata.gz: 4bdd6c53f65cb096de1c2da724a735ecee7ceaed464627f69adbb816b3afa20c85a202cce201a7ee5d35e52ae31663c7f8a9c02d47bc11ea7ead86cebfb0972f
7
+ data.tar.gz: 4664e2b6e724f3bba469ef1907b82fe25d90761251ff5cabb204bd75e1a64b522ba3a438438a76e5d58e80952870334abb8194d9d0199b6e2addb2143120b5fa
@@ -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
 
@@ -48,7 +48,7 @@ module DutyFree
48
48
  # The snake-cased column name to be used for building the full list of template_columns
49
49
  def sym_string
50
50
  @sym_string ||= ::DutyFree::Util._prefix_join(
51
- [pre_prefix, prefix, ::DutyFree::Util._clean_name(name, import_columns_as)],
51
+ [pre_prefix, prefix, ::DutyFree::Util._clean_name(name, import_template_as)],
52
52
  '_'
53
53
  ).tr('.', '_')
54
54
  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
@@ -21,22 +18,40 @@ module DutyFree
21
18
  # end
22
19
 
23
20
  # Export at least column header, and optionally include all existing data as well
24
- def df_export(is_with_data = true, import_columns = nil)
21
+ def df_export(is_with_data = true, import_template = nil)
25
22
  # 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
23
+ if is_with_data.is_a?(Hash) && !import_template
24
+ import_template = is_with_data
28
25
  is_with_data = true
29
26
  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)]
27
+ import_template ||= if constants.include?(:IMPORT_TEMPLATE)
28
+ self::IMPORT_TEMPLATE
29
+ else
30
+ suggest_template(0, false, false)
31
+ end
32
+
33
+ # Friendly column names that end up in the first row of the CSV
34
+ # Required columns get prefixed with a *
35
+ requireds = (import_template[:required] || [])
36
+ rows = ::DutyFree::Extensions._template_columns(self, import_template).map do |col|
37
+ is_required = requireds.include?(col)
38
+ col = col.to_s.titleize
39
+ # Alias-ify the full column names
40
+ aliases = (import_template[:as] || [])
41
+ aliases.each do |k, v|
42
+ if col.start_with?(v)
43
+ col = k + col[v.length..-1]
44
+ break
45
+ end
46
+ end
47
+ (is_required ? '* ' : '') + col
48
+ end
49
+ rows = Array(rows)
50
+
36
51
  if is_with_data
37
52
  # 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)
53
+ template_cols, template_joins = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template)
54
+ relation = left_joins(template_joins)
40
55
 
41
56
  # So we can properly create the SELECT list, create a mapping between our
42
57
  # column alias prefixes and the aliases AREL creates.
@@ -46,7 +61,7 @@ module DutyFree
46
61
  mapping = our_names.zip(arel_alias_names).to_h
47
62
 
48
63
  relation.select(template_cols.map { |x| x.to_s(mapping) }).each do |result|
49
- rows << template_columns(import_columns).map do |col|
64
+ rows << ::DutyFree::Extensions._template_columns(self, import_template).map do |col|
50
65
  value = result.send(col)
51
66
  case value
52
67
  when true
@@ -63,12 +78,16 @@ module DutyFree
63
78
  end
64
79
 
65
80
  # 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
81
+ def df_import(data, import_template = nil)
82
+ self.instance_variable_set(:@defined_uniques, nil)
83
+ self.instance_variable_set(:@valid_uniques, nil)
84
+
85
+ import_template ||= if constants.include?(:IMPORT_TEMPLATE)
86
+ self::IMPORT_TEMPLATE
87
+ else
88
+ suggest_template(0, false, false)
89
+ end
90
+ puts "Chose #{import_template}"
72
91
  inserts = []
73
92
  updates = []
74
93
  counts = Hash.new { |h, k| h[k] = [] }
@@ -79,17 +98,18 @@ module DutyFree
79
98
  cols = nil
80
99
  starred = []
81
100
  partials = []
82
- all = import_columns[:all]
101
+ all = import_template[:all]
83
102
  keepers = {}
84
103
  valid_unique = nil
85
104
  existing = {}
86
105
  devise_class = ''
106
+ ret = nil
87
107
 
88
108
  reference_models = if Object.const_defined?('Apartment')
89
109
  Apartment.excluded_models
90
110
  else
91
111
  []
92
- end
112
+ end
93
113
 
94
114
  if Object.const_defined?('Devise')
95
115
  Object.const_get('Devise') # If this fails, devise_class will remain a blank string.
@@ -123,13 +143,14 @@ module DutyFree
123
143
  # Will show as just one transaction when using auditing solutions such as PaperTrail
124
144
  ActiveRecord::Base.transaction do
125
145
  # 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)
146
+ if before_import ||= (import_template[:before_import]) # || some generic before_import)
127
147
  before_import.call(data)
128
148
  end
149
+ col_list = nil
129
150
  data.each_with_index do |row, row_num|
130
151
  row_errors = {}
131
152
  if is_first # Anticipate that first row has column names
132
- uniques = import_columns[:uniques]
153
+ uniques = import_template[:uniques]
133
154
 
134
155
  # Look for UTF-8 BOM in very first cell
135
156
  row[0] = row[0][3..-1] if row[0].start_with?([239, 187, 191].pack('U*'))
@@ -154,10 +175,13 @@ module DutyFree
154
175
  col.strip!
155
176
  end
156
177
  end
157
- defined_uniques(uniques, cols, starred)
158
- cols.map! { |col| ::DutyFree::Util._clean_name(col, import_columns[:as]) } # %%%
178
+ # %%% Will the uniques saved into @defined_uniques here just get redefined later
179
+ # after the next line, the map! with clean to change out the alias names? So we can't yet set
180
+ # col_list?
181
+ defined_uniques(uniques, cols, cols.join('|'), starred)
182
+ cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) } # %%%
159
183
  # 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
184
+ template_column_objects = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template).first
161
185
  cols.each_with_index do |col, idx|
162
186
  # prefixes = col_detail.pre_prefix + (col_detail.prefix.blank? ? [] : [col_detail.prefix])
163
187
  keepers[idx] = template_column_objects.find { |col_obj| col_obj.titleize == col }
@@ -168,18 +192,25 @@ module DutyFree
168
192
  end
169
193
 
170
194
  # Returns just the first valid unique lookup set if there are multiple
171
- valid_unique = valid_uniques(uniques, cols, starred, import_columns)
195
+ valid_unique = find_existing(uniques, cols, starred, import_template, keepers, false)
172
196
  # 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; }
197
+ existing = pluck(*([:id] + valid_unique.keys)).each_with_object(existing) do |v, s|
198
+ s[v[1..-1].map(&:to_s)] = v.first
199
+ s
200
+ end
174
201
  is_first = false
175
202
  else # Normal row of data
176
203
  is_insert = false
177
204
  is_do_save = true
178
205
  existing_unique = valid_unique.inject([]) do |s, v|
179
- s << row[v.last].to_s
206
+ s << if v.last.is_a?(Array)
207
+ v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck('MAX(id)').first.to_s
208
+ else
209
+ row[v.last].to_s
210
+ end
180
211
  end
181
212
  # Check to see if they want to preprocess anything
182
- if @before_process ||= import_columns[:before_process]
213
+ if @before_process ||= import_template[:before_process]
183
214
  existing_unique = @before_process.call(valid_unique, existing_unique)
184
215
  end
185
216
  obj = if existing.include?(existing_unique)
@@ -200,17 +231,22 @@ module DutyFree
200
231
 
201
232
  # Not the same as the last path?
202
233
  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
234
+ # puts sub_obj.class.name
235
+ if respond_to?(:around_import_save)
236
+ # Send them the sub_obj even if it might be invalid so they can choose
237
+ # to make it valid if they wish.
238
+ # binding.pry
239
+ around_import_save(sub_obj) do |modded_obj = nil|
240
+ modded_obj = (modded_obj || sub_obj)
241
+ modded_obj.save if sub_obj&.valid?
209
242
  end
243
+ elsif sub_obj&.valid?
244
+ # binding.pry if sub_obj.is_a?(Employee) && sub_obj.first_name == 'Andrew'
245
+ sub_obj.save
210
246
  end
211
247
  end
212
248
  sub_obj = obj
213
- this_path = ''
249
+ this_path = +''
214
250
  v.path.each_with_index do |path_part, idx|
215
251
  this_path << (this_path.blank? ? path_part.to_s : ",#{path_part}")
216
252
  unless (sub_next = sub_objects[this_path])
@@ -240,20 +276,36 @@ module DutyFree
240
276
  klass.new
241
277
  else
242
278
  # Try to find a unique item if one is referenced
243
- trim_prefix = v.titleize[0..-(v.name.length + 2)]
279
+ sub_bt = nil
244
280
  begin
245
- sub_unique = assoc.klass.valid_uniques(uniques, cols, starred, import_columns, all, trim_prefix)
281
+ # Goofs up if trim_prefix isn't the same name as the class, or if it's
282
+ # a self-join? (like when trim_prefix == 'Reports To')
283
+ # %%% Need to test this more when the self-join is more than one hop away,
284
+ # such as importing orders and having employees come along :)
285
+ # if sub_obj.class == klass
286
+ # trim_prefix = ''
287
+ # # binding.pry
288
+ # end
289
+ # %%% Maybe instead of passing in "klass" we can give the belongs_to association and build through that instead,
290
+ # allowing us to nix the klass.new(criteria) line below.
291
+ trim_prefix = v.titleize[0..-(v.name.length + 2)]
292
+ trim_prefix << ' ' unless trim_prefix.blank?
293
+ if klass == sub_obj.class # Self-referencing thing pointing to us?
294
+ # %%% This should be more general than just for self-referencing things.
295
+ sub_cols = cols.map { |c| c.start_with?(trim_prefix) ? c[trim_prefix.length..-1] : nil }
296
+ sub_bt, criteria = klass.find_existing(uniques, sub_cols, starred, import_template, keepers, assoc, row, klass, all, trim_prefix)
297
+ else
298
+ sub_bt, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, nil, row, klass, all, trim_prefix)
299
+ end
246
300
  rescue ::DutyFree::NoUniqueColumnError
247
301
  sub_unique = nil
248
302
  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 || {}))
303
+ # binding.pry if sub_obj.is_a?(Employee) && sub_obj.first_name == 'Nancy' &&
304
+ # sub_bt.is_a?(Employee)
305
+ # %%% Can criteria really ever be nil anymore?
306
+ sub_bt ||= klass.new(criteria || {})
307
+ sub_obj.send("#{path_part}=", sub_bt)
308
+ sub_bt # unless klass == sub_obj.class # Don't go further if it's self-referencing
257
309
  end
258
310
  end
259
311
  # Look for possible missing polymorphic detail
@@ -266,25 +318,16 @@ module DutyFree
266
318
  if sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
267
319
  # Try to find a unique item if one is referenced
268
320
  # %%% 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.
321
+ # in IMPORT_TEMPLATE[:all] that this will break. Need to test deeply nested things.
270
322
  start = (v.pre_prefix.blank? ? 0 : v.pre_prefix.length)
271
323
  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?
324
+ trim_prefix << ' ' unless trim_prefix.blank?
325
+ klass = sub_next.klass
326
+ # binding.pry if klass.name == 'OrderDetail'
327
+
328
+ # assoc.inverse_of is the belongs_to side of the has_many train we came in here on.
329
+ sub_hm, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
330
+
288
331
  # If still not found then create a new related object using this has_many collection
289
332
  sub_next = sub_hm || sub_next.new(criteria)
290
333
  end
@@ -305,11 +348,10 @@ module DutyFree
305
348
  next if sub_obj.nil?
306
349
 
307
350
  sym = "#{v.name}=".to_sym
308
- sub_class = sub_obj.class
309
351
  next unless sub_obj.respond_to?(sym)
310
352
 
311
- col_type = sub_class.columns_hash[v.name.to_s]&.type
312
- if col_type.nil? && (virtual_columns = import_columns[:virtual_columns]) &&
353
+ col_type = (sub_class = sub_obj.class).columns_hash[v.name.to_s]&.type
354
+ if col_type.nil? && (virtual_columns = import_template[:virtual_columns]) &&
313
355
  (virtual_columns = virtual_columns[this_path] || virtual_columns)
314
356
  col_type = virtual_columns[v.name]
315
357
  end
@@ -347,13 +389,18 @@ module DutyFree
347
389
  end
348
390
  end
349
391
 
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
392
+ # Give a window of opportunity to tweak user objects controlled by Devise
393
+ obj_class = obj.class
394
+ is_do_save = if obj_class.respond_to?(:before_devise_save) && obj_class.name == devise_class
395
+ obj_class.before_devise_save(obj, existing)
396
+ else
397
+ true
398
+ end
352
399
 
353
400
  if obj.valid?
354
401
  obj.save if is_do_save
355
402
  # 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) }
403
+ existing_unique = valid_unique.keys.inject([]) { |s, v| s << obj.send(v).to_s }
357
404
  # Update the duplicate counts and inserted / updated results
358
405
  counts[existing_unique] << row_num
359
406
  (is_insert ? inserts : updates) << { row_num => existing_unique } if is_do_save
@@ -365,65 +412,40 @@ module DutyFree
365
412
  errors << { row_num => row_errors } unless row_errors.empty?
366
413
  end
367
414
  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
415
+ duplicates = counts.each_with_object([]) do |v, s|
416
+ s += v.last[1..-1].map { |line_num| { line_num => v.first } } if v.last.count > 1
417
+ s
370
418
  end
371
- # Check to see if they want to do anything before the whole import
419
+ # Check to see if they want to do anything after the import
372
420
  ret = { inserted: inserts, updated: updates, duplicates: duplicates, errors: errors }
373
- if @after_import ||= (import_columns[:after_import]) # || some generic after_import)
421
+ if @after_import ||= (import_template[:after_import]) # || some generic after_import)
374
422
  ret = ret2 if (ret2 = @after_import.call(ret)).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
394
- end
395
- (is_required ? '* ' : '') + col
396
- end
397
- end
398
-
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
407
-
408
428
  # For use with importing, based on the provided column list calculate all valid combinations
409
429
  # 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)
430
+ # Returns an object found by this means.
431
+ def find_existing(uniques, cols, starred, import_template, keepers, train_we_came_in_here_on, row = nil, obj = nil, all = nil, trim_prefix = '')
432
+ col_name_offset = trim_prefix.length
412
433
  @valid_uniques ||= {} # Fancy memoisation
413
434
  col_list = cols.join('|')
414
435
  unless (vus = @valid_uniques[col_list])
415
436
  # Find all unique combinations that are available based on incoming columns, and
416
437
  # pair them up with column number mappings.
417
- template_column_objects = recurse_def(all || import_columns[:all], import_columns).first
438
+ template_column_objects = ::DutyFree::Extensions._recurse_def(self, all || import_template[:all], import_template).first
418
439
  available = if trim_prefix.blank?
419
440
  template_column_objects.select { |col| col.pre_prefix.blank? && col.prefix.blank? }
420
441
  else
421
442
  trim_prefix_snake = trim_prefix.downcase.tr(' ', '_')
422
443
  template_column_objects.select do |col|
423
- trim_prefix_snake == ::DutyFree::Util._prefix_join([col.pre_prefix, col.prefix], '_').tr('.', '_')
444
+ this_prefix = ::DutyFree::Util._prefix_join([col.pre_prefix, col.prefix], '_').tr('.', '_')
445
+ trim_prefix_snake == "#{this_prefix}_"
424
446
  end
425
447
  end.map { |avail| avail.name.to_s.titleize }
426
- vus = defined_uniques(uniques, cols, starred).select do |k, _v|
448
+ vus = defined_uniques(uniques, cols, nil, starred).select do |k, _v|
427
449
  is_good = true
428
450
  k.each do |k_col|
429
451
  unless k_col.start_with?(trim_prefix) && available.include?(k_col[col_name_offset..-1])
@@ -437,26 +459,91 @@ module DutyFree
437
459
  end
438
460
 
439
461
  # 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
462
  ret = {}
447
- key.each_with_index do |k, idx|
448
- ret[k[col_name_offset..-1].downcase.tr(' ', '_').to_sym] = val[idx]
463
+ unless vus.empty? # raise NoUniqueColumnError.new(I18n.t('import.no_unique_column_error'))
464
+ # Convert the first entry to a simplified hash, such as:
465
+ # {[:investigator_institutions_name, :investigator_institutions_email] => [8, 9], ...}
466
+ # to {:name => 8, :email => 9}
467
+ key, val = vus.first
468
+ key.each_with_index do |k, idx|
469
+ ret[k[col_name_offset..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
470
+ end
449
471
  end
450
- ret
472
+
473
+ # %%% If uniqueness is based on something else hanging out on a belongs_to then we're pretty hosed.
474
+ # (Case in point, importing Order with related Order Detail and Product, and then Product needs to
475
+ # be found or built first before OrderDetail.)
476
+ # Might have to do a deferred save kind of thing, and also make sure the product stuff came first
477
+ # before the other stuff
478
+
479
+ # Add in any foreign key stuff we can find from other belongs_to associations
480
+ # %%% This is starting to look like the other BelongsToAssociation code above around line
481
+ # 697, so it really needs to be turned into something recursive instead of this two-layer
482
+ # thick thing at best.
483
+ bts = reflect_on_all_associations.each_with_object([]) do |sn_assoc, s|
484
+ if sn_assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection) &&
485
+ (!train_we_came_in_here_on || sn_assoc != train_we_came_in_here_on) &&
486
+ sn_assoc.klass != self # Omit stuff pointing to us (like self-referencing stuff)
487
+ # %%% Make sure there's a starred column we know about from this one
488
+ ret[sn_assoc.foreign_key] = nil if train_we_came_in_here_on == false
489
+ s << sn_assoc
490
+ end
491
+ s
492
+ end
493
+
494
+ # Find by all corresponding columns
495
+ criteria = {}
496
+ if train_we_came_in_here_on != false
497
+ criteria = ret.each_with_object({}) do |v, s|
498
+ s[v.first.to_sym] = row[v.last]
499
+ s
500
+ end
501
+ end
502
+
503
+ bts.each do |sn_bt|
504
+ # This search prefix becomes something like "Order Details Product "
505
+ cols.each_with_index do |bt_col, idx|
506
+ next unless bt_col.start_with?(trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} ")
507
+
508
+ fk_id = if row
509
+ # Max ID so if there are multiple, only the most recent one is picked.
510
+ # %%% Need to stack these up in case there are multiple (like first_name, last_name on a referenced employee)
511
+ sn_bt.klass.where(keepers[idx].name => row[idx]).limit(1).pluck('MAX(id)').first
512
+ else
513
+ [sn_bt.klass, keepers[idx].name, idx]
514
+ end
515
+ criteria[sn_bt.foreign_key] = fk_id
516
+ end
517
+ end
518
+
519
+ # Short-circuiting this to only get back the valid_uniques?
520
+ return ret.merge(criteria) if train_we_came_in_here_on == false
521
+
522
+ # With this criteria, find any matching has_many row we can so we can update it
523
+ sub_hm = obj.find do |hm_obj|
524
+ is_good = true
525
+ criteria.each do |k, v|
526
+ if hm_obj.send(k).to_s != v.to_s
527
+ is_good = false
528
+ break
529
+ end
530
+ end
531
+ is_good
532
+ end
533
+ # Try looking it up through ActiveRecord
534
+ # %%% Should we perhaps do this first before the more intensive find routine above?
535
+ sub_hm = obj.find_by(criteria) if sub_hm.nil?
536
+ [sub_hm, criteria]
451
537
  end
452
538
 
453
539
  private
454
540
 
455
- def defined_uniques(uniques, cols = [], starred = [])
541
+ def defined_uniques(uniques, cols = [], col_list = nil, starred = [])
542
+ col_list ||= cols.join('|')
456
543
  @defined_uniques ||= {}
457
- unless (defined_uniques = @defined_uniques[cols])
544
+ unless (defined_uniq = @defined_uniques[col_list])
458
545
  utilised = {} # Track columns that have been referenced thusfar
459
- defined_uniques = uniques.each_with_object({}) do |unique, s|
546
+ defined_uniq = uniques.each_with_object({}) do |unique, s|
460
547
  if unique.is_a?(Array)
461
548
  key = []
462
549
  value = []
@@ -479,42 +566,56 @@ module DutyFree
479
566
  end
480
567
  end
481
568
  end
482
- (starred - utilised.keys).each { |star| defined_uniques[[star]] = [cols.index(star)] }
483
- @defined_uniques[cols] = defined_uniques
569
+ (starred - utilised.keys).each { |star| defined_uniq[[star]] = [cols.index(star)] }
570
+ @defined_uniques[col_list] = defined_uniq
484
571
  end
485
- defined_uniques
572
+ defined_uniq
486
573
  end
574
+ end # module ClassMethods
487
575
 
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]
576
+ # The snake-cased column alias names used in the query to export data
577
+ def self._template_columns(klass, import_template = nil)
578
+ template_detail_columns = klass.instance_variable_get(:@template_detail_columns)
579
+ if (template_import_columns = klass.instance_variable_get(:@template_import_columns)) != import_template
580
+ klass.instance_variable_set(:@template_import_columns, template_import_columns = import_template)
581
+ klass.instance_variable_set(:@template_detail_columns, (template_detail_columns = nil))
516
582
  end
517
- end # module ClassMethods
583
+ unless template_detail_columns
584
+ puts "* Redoing *"
585
+ template_detail_columns = _recurse_def(klass, import_template[:all], import_template).first.map(&:to_sym)
586
+ klass.instance_variable_set(:@template_detail_columns, template_detail_columns)
587
+ end
588
+ template_detail_columns
589
+ end
590
+
591
+ # Recurse and return two arrays -- one with all columns in sequence, and one a hierarchy of
592
+ # nested hashes to be used with ActiveRecord's .joins() to facilitate export.
593
+ def self._recurse_def(klass, array, import_template, assocs = [], joins = [], pre_prefix = '', prefix = '')
594
+ # Confirm we can actually navigate through this association
595
+ prefix_assoc = (assocs.last&.klass || klass).reflect_on_association(prefix) if prefix.present?
596
+ assocs = assocs.dup << prefix_assoc unless prefix_assoc.nil?
597
+ prefixes = ::DutyFree::Util._prefix_join([pre_prefix, prefix])
598
+ array = array.inject([]) do |s, col|
599
+ s + if col.is_a?(Hash)
600
+ col.inject([]) do |s2, v|
601
+ joins << { v.first.to_sym => (joins_array = []) }
602
+ s2 += _recurse_def(klass, (v.last.is_a?(Array) ? v.last : [v.last]), import_template, assocs, joins_array, prefixes, v.first.to_sym).first
603
+ end
604
+ elsif col.nil?
605
+ if assocs.empty?
606
+ []
607
+ else
608
+ # Bring in from another class
609
+ joins << { prefix => (joins_array = []) }
610
+ # %%% Also bring in uniques and requireds
611
+ _recurse_def(klass, assocs.last.klass::IMPORT_TEMPLATE[:all], import_template, assocs, joins_array, prefixes).first
612
+ end
613
+ else
614
+ [::DutyFree::Column.new(col, pre_prefix, prefix, assocs, klass, import_template[:as])]
615
+ end
616
+ end
617
+ [array, joins]
618
+ end
518
619
  end # module Extensions
519
620
 
520
621
  class NoUniqueColumnError < ActiveRecord::RecordNotUnique
@@ -167,7 +167,7 @@ module DutyFree
167
167
  # belongs_to is more involved since there may be multiple foreign keys which point
168
168
  # from the foreign table to this primary one, so exclude all these links.
169
169
  _find_belongs_tos(assoc.first.last, assoc.last, errored_assocs).map do |f_assoc|
170
- [f_assoc.foreign_key.to_s, f_assoc.active_record]
170
+ [[f_assoc.foreign_key.to_s], f_assoc.active_record]
171
171
  end
172
172
  end
173
173
  # puts "New Poison: #{new_poison_links.inspect}"
@@ -247,11 +247,11 @@ module DutyFree
247
247
  end
248
248
  end
249
249
 
250
- # Show a "pretty" version of IMPORT_COLUMNS, to be placed in a model
250
+ # Show a "pretty" version of IMPORT_TEMPLATE, to be placed in a model
251
251
  def self._template_pretty_print(template, indent = 0, child_count = 0, is_hash_in_hash = false)
252
252
  unless indent.negative?
253
253
  if indent.zero?
254
- print 'IMPORT_COLUMNS = '
254
+ print 'IMPORT_TEMPLATE = '
255
255
  else
256
256
  puts unless is_hash_in_hash
257
257
  end
@@ -305,7 +305,7 @@ module DutyFree
305
305
  if indent == 2
306
306
  puts
307
307
  indent = 0
308
- puts '}'
308
+ puts '}.freeze'
309
309
  elsif indent >= 0
310
310
  print "#{' ' unless child_count.zero?}}"
311
311
  end
@@ -57,11 +57,11 @@ module DutyFree
57
57
  prefixes.reject(&:blank?).join(separator || '.')
58
58
  end
59
59
 
60
- def self._clean_name(name, import_columns_as)
60
+ def self._clean_name(name, import_template_as)
61
61
  return name if name.is_a?(Symbol)
62
62
 
63
63
  # Expand aliases
64
- (import_columns_as || []).each do |k, v|
64
+ (import_template_as || []).each do |k, v|
65
65
  if (k[-1] == ' ' && name.start_with?(k)) || name == k
66
66
  name.replace(v + name[k.length..-1])
67
67
  break
@@ -5,7 +5,7 @@ module DutyFree
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 0
8
+ TINY = 1
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
@@ -1,2 +1,2 @@
1
1
  Description:
2
- Generates (but does not run) a migration to add a versions table.
2
+ Modifies an existing model so that it includes an IMPORT_TEMPLATE.
@@ -4,18 +4,10 @@ require 'rails/generators'
4
4
  require 'rails/generators/active_record'
5
5
 
6
6
  module DutyFree
7
- # Installs DutyFree in a rails app.
7
+ # Auto-generates an IMPORT_TEMPLATE entry for one or more models
8
8
  class InstallGenerator < ::Rails::Generators::Base
9
9
  include ::Rails::Generators::Migration
10
10
 
11
- # Class names of MySQL adapters.
12
- # - `MysqlAdapter` - Used by gems: `mysql`, `activerecord-jdbcmysql-adapter`.
13
- # - `Mysql2Adapter` - Used by `mysql2` gem.
14
- MYSQL_ADAPTERS = [
15
- 'ActiveRecord::ConnectionAdapters::MysqlAdapter',
16
- 'ActiveRecord::ConnectionAdapters::Mysql2Adapter'
17
- ].freeze
18
-
19
11
  source_root File.expand_path('templates', __dir__)
20
12
  class_option(
21
13
  :with_changes,
@@ -68,8 +60,14 @@ module DutyFree
68
60
  "[#{major}.#{ActiveRecord::VERSION::MINOR}]"
69
61
  end
70
62
 
63
+ # Class names of MySQL adapters.
64
+ # - `MysqlAdapter` - Used by gems: `mysql`, `activerecord-jdbcmysql-adapter`.
65
+ # - `Mysql2Adapter` - Used by `mysql2` gem.
71
66
  def mysql?
72
- MYSQL_ADAPTERS.include?(ActiveRecord::Base.connection.class.name)
67
+ [
68
+ 'ActiveRecord::ConnectionAdapters::MysqlAdapter',
69
+ 'ActiveRecord::ConnectionAdapters::Mysql2Adapter'
70
+ ].freeze.include?(ActiveRecord::Base.connection.class.name)
73
71
  end
74
72
 
75
73
  # Even modern versions of MySQL still use `latin1` as the default character
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.0
4
+ version: 1.0.1
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-10-29 00:00:00.000000000 Z
11
+ date: 2020-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord