datashift 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/README.markdown +91 -55
  3. data/VERSION +1 -1
  4. data/datashift.gemspec +8 -23
  5. data/lib/applications/jexcel_file.rb +1 -2
  6. data/lib/datashift.rb +34 -15
  7. data/lib/datashift/column_packer.rb +98 -34
  8. data/lib/datashift/data_transforms.rb +83 -0
  9. data/lib/datashift/delimiters.rb +58 -10
  10. data/lib/datashift/excel_base.rb +123 -0
  11. data/lib/datashift/exceptions.rb +45 -7
  12. data/lib/datashift/load_object.rb +25 -0
  13. data/lib/datashift/mapping_service.rb +91 -0
  14. data/lib/datashift/method_detail.rb +40 -62
  15. data/lib/datashift/method_details_manager.rb +18 -2
  16. data/lib/datashift/method_dictionary.rb +27 -10
  17. data/lib/datashift/method_mapper.rb +49 -41
  18. data/lib/datashift/model_mapper.rb +42 -22
  19. data/lib/datashift/populator.rb +258 -143
  20. data/lib/datashift/thor_base.rb +38 -0
  21. data/lib/exporters/csv_exporter.rb +57 -145
  22. data/lib/exporters/excel_exporter.rb +73 -60
  23. data/lib/generators/csv_generator.rb +65 -5
  24. data/lib/generators/generator_base.rb +69 -3
  25. data/lib/generators/mapping_generator.rb +112 -0
  26. data/lib/helpers/core_ext/csv_file.rb +33 -0
  27. data/lib/loaders/csv_loader.rb +41 -39
  28. data/lib/loaders/excel_loader.rb +130 -116
  29. data/lib/loaders/loader_base.rb +190 -146
  30. data/lib/loaders/paperclip/attachment_loader.rb +4 -4
  31. data/lib/loaders/paperclip/datashift_paperclip.rb +5 -3
  32. data/lib/loaders/paperclip/image_loading.rb +9 -7
  33. data/lib/loaders/reporter.rb +17 -8
  34. data/lib/thor/export.thor +12 -13
  35. data/lib/thor/generate.thor +1 -9
  36. data/lib/thor/import.thor +13 -24
  37. data/lib/thor/mapping.thor +65 -0
  38. data/spec/Gemfile +13 -11
  39. data/spec/Gemfile.lock +98 -93
  40. data/spec/csv_exporter_spec.rb +104 -99
  41. data/spec/csv_generator_spec.rb +159 -0
  42. data/spec/csv_loader_spec.rb +197 -16
  43. data/spec/datashift_spec.rb +9 -0
  44. data/spec/excel_exporter_spec.rb +149 -58
  45. data/spec/excel_generator_spec.rb +35 -44
  46. data/spec/excel_loader_spec.rb +196 -178
  47. data/spec/excel_spec.rb +8 -5
  48. data/spec/loader_base_spec.rb +47 -7
  49. data/spec/mapping_spec.rb +117 -0
  50. data/spec/method_dictionary_spec.rb +24 -11
  51. data/spec/method_mapper_spec.rb +5 -7
  52. data/spec/model_mapper_spec.rb +41 -0
  53. data/spec/paperclip_loader_spec.rb +3 -6
  54. data/spec/populator_spec.rb +48 -14
  55. data/spec/spec_helper.rb +85 -73
  56. data/spec/thor_spec.rb +40 -5
  57. metadata +93 -86
  58. data/lib/applications/excel_base.rb +0 -63
@@ -14,183 +14,287 @@ require 'logging'
14
14
 
15
15
  module DataShift
16
16
 
17
+ Struct.new("Substitution", :pattern, :replacement)
18
+
17
19
  class Populator
18
-
20
+
19
21
  include DataShift::Logging
20
-
22
+
21
23
  def self.insistent_method_list
22
24
  @insistent_method_list ||= [:to_s, :to_i, :to_f, :to_b]
23
25
  end
24
-
26
+
25
27
  # When looking up an association, when no field provided, try each of these in turn till a match
26
28
  # i.e find_by_name, find_by_title, find_by_id
27
29
  def self.insistent_find_by_list
28
30
  @insistent_find_by_list ||= [:name, :title, :id]
29
31
  end
30
-
31
-
32
+
33
+
34
+ # Default data embedded in column headings - so effectively apply globally
35
+ # to teh whole column - hence class methods
36
+ def self.set_header_default_data(operator, data )
37
+ header_default_data[operator] = data
38
+ end
39
+
40
+ def self.header_default_data
41
+ @header_default_data ||= {}
42
+ end
43
+
44
+
32
45
  attr_reader :current_value, :original_value_before_override
46
+ attr_reader :current_col_type
47
+
33
48
  attr_reader :current_attribute_hash
34
49
  attr_reader :current_method_detail
35
-
36
- def initialize
50
+
51
+ def initialize
37
52
  @current_value = nil
53
+ @current_method_detail = nil
38
54
  @original_value_before_override = nil
39
55
  @current_attribute_hash = {}
40
56
 
41
57
  end
42
-
43
- # Set member variables to hold details, value and optional attributes.
58
+
59
+ # Convert DSL string forms into a hash
60
+ # e.g
61
+ #
62
+ # "{:name => 'autechre'}" => Hash['name'] = autechre'
63
+ # "{:cost_price => '13.45', :price => 23, :sale_price => 4.23 }"
64
+
65
+ def self.string_to_hash( str )
66
+ h = {}
67
+ str.gsub(/[{}:]/,'').split(', ').map do |e|
68
+ k,v = e.split('=>')
69
+
70
+ k.strip!
71
+ v.strip!
72
+
73
+ if( v.match(/['"]/) )
74
+ h[k] = v.gsub(/["']/, '')
75
+ elsif( v.match(/^\d+$|^\d*\.\d+$|^\.\d+$/) )
76
+ h[k] = v.to_f
77
+ else
78
+ h[k] = v
79
+ end
80
+ h
81
+ end
82
+
83
+ h
84
+ end
85
+
86
+ # Set member variables to hold details, value and optional attributes,
87
+ # to be set on the 'value' once created
44
88
  #
45
89
  # Check supplied value, validate it, and if required :
46
90
  # set to provided default value
47
91
  # prepend any provided prefixes
48
92
  # add any provided postfixes
49
93
  def prepare_data(method_detail, value)
50
-
51
- @current_value, @current_attribute_hash = value.to_s.split(Delimiters::attribute_list_start)
52
-
53
- if(@current_attribute_hash)
54
- @current_attribute_hash.strip!
55
- puts "DEBUG: Populator Value contains additional attributes"
56
- @current_attribute_hash = nil unless @current_attribute_hash.include?('}')
57
- end
58
-
59
- @current_attribute_hash ||= {}
60
-
61
- @current_method_detail = method_detail
62
-
63
- operator = method_detail.operator
64
-
65
- override_value(operator)
66
-
67
- if((value.nil? || value.to_s.empty?) && default_value(operator))
68
- @current_value = default_value(operator)
94
+
95
+ raise NilDataSuppliedError.new("No method detail supplied for prepare_data") unless(method_detail)
96
+
97
+ begin
98
+ @prepare_data_const_regexp ||= Regexp.new( Delimiters::attribute_list_start + ".*" + Delimiters::attribute_list_end)
99
+
100
+ if( value.is_a? ActiveRecord::Relation ) # Rails 4 - query no longer returns an array
101
+ @current_value = value.to_a
102
+ elsif( !DataShift::Guards.jruby? && value.class.ancestors.include?(Spreadsheet::Formula))
103
+ @current_value = value.value
104
+ elsif( value.class.ancestors.include?(ActiveRecord::Base) || value.is_a?(Array))
105
+ @current_value = value
106
+ else
107
+ @current_value = value.to_s
108
+
109
+ attribute_hash = @current_value.slice!(@prepare_data_const_regexp)
110
+
111
+ if(attribute_hash)
112
+ @current_attribute_hash = Populator::string_to_hash( attribute_hash )
113
+ logger.info "Populator for #{@current_value} has attributes #{@current_attribute_hash.inspect}"
114
+ end
115
+ end
116
+
117
+ @current_attribute_hash ||= {}
118
+
119
+ @current_method_detail = method_detail
120
+
121
+ @current_col_type = @current_method_detail.col_type
122
+
123
+ operator = method_detail.operator
124
+
125
+ if(has_override?(operator))
126
+ override_value(operator) # takes precedence over anything else
127
+ else
128
+ # if no value check for a defaults from config, headers
129
+ if(default_value(operator))
130
+ @current_value = default_value(operator)
131
+ elsif(Populator::header_default_data[operator])
132
+ @current_value = Populator::header_default_data[operator].to_s
133
+ elsif(Populator::header_default_data[operator])
134
+ @current_value = Populator::header_default_data[operator].to_s
135
+ elsif(method_detail.find_by_value)
136
+ @current_value = method_detail.find_by_value
137
+ end if(value.nil? || value.to_s.empty?)
138
+ end
139
+
140
+ substitute( operator )
141
+
142
+ @current_value = "#{prefix(operator)}#{@current_value}" if(prefix(operator))
143
+ @current_value = "#{@current_value}#{postfix(operator)}" if(postfix(operator))
144
+
145
+ rescue => e
146
+ logger.error("populator failed to prepare data supplied for operator #{method_detail.operator}")
147
+ logger.error("populator error: #{e.inspect}")
148
+ logger.error("populator stacktrace: #{e.backtrace.last}")
149
+ raise DataProcessingError.new("opulator failed to prepare data #{value} for operator #{method_detail.operator}")
69
150
  end
70
-
71
- @current_value = "#{prefix(operator)}#{@current_value}" if(prefix(operator))
72
- @current_value = "#{@current_value}#{postfix(operator)}" if(postfix(operator))
73
151
 
74
152
  return @current_value, @current_attribute_hash
75
153
  end
76
-
77
- def assign(method_detail, record, value )
78
-
79
- @current_value = value
80
-
81
- # Rails 4 - not an array any more
82
- if( value.is_a? ActiveRecord::Relation )
83
- logger.warn("Relation passed rather than value #{value.inspect}")
84
- @current_value = value.to_a
85
- end
86
154
 
87
- # logger.info("WARNING nil value supplied for Column [#{@name}]") if(@current_value.nil?)
155
+ # Main client hook
156
+
157
+ def prepare_and_assign(method_detail, record, value)
158
+
159
+ prepare_data(method_detail, value)
160
+
161
+ assign(record)
162
+ end
163
+
164
+ def assign(record)
165
+
166
+ raise NilDataSuppliedError.new("No method detail - cannot assign data") unless(current_method_detail)
167
+
168
+ operator = current_method_detail.operator
169
+
170
+ logger.debug("Populator assign - [#{current_value}] via #{current_method_detail.operator} (#{current_method_detail.operator_type})")
171
+
172
+ if( current_method_detail.operator_for(:belongs_to) )
173
+
174
+ insistent_belongs_to(current_method_detail, record, current_value)
175
+
176
+ elsif( current_method_detail.operator_for(:has_many) )
88
177
 
89
- operator = method_detail.operator
90
-
91
- if( method_detail.operator_for(:belongs_to) )
92
-
93
- #puts "DEBUG : BELONGS_TO : #{@name} : #{operator} - Lookup #{@current_value} in DB"
94
- insistent_belongs_to(method_detail, record, @current_value)
95
-
96
- elsif( method_detail.operator_for(:has_many) )
97
-
98
- puts "DEBUG : VALUE TYPE [#{value.class.name.include?(operator.classify)}] [#{ModelMapper.class_from_string(value.class.name)}]" unless(value.is_a?(Array))
99
-
100
178
  # The include? check is best I can come up with right now .. to handle module/namespaces
101
179
  # TODO - can we determine the real class type of an association
102
180
  # e.g given a association taxons, which operator.classify gives us Taxon, but actually it's Spree::Taxon
103
181
  # so how do we get from 'taxons' to Spree::Taxons ? .. check if further info in reflect_on_all_associations
104
182
 
105
- if(@current_value.is_a?(Array) || @current_value.class.name.include?(operator.classify))
106
- record.send(operator) << @current_value
107
- else
108
- puts "ERROR #{@current_value.class} - Not expected type for has_many #{operator} - cannot assign"
183
+ begin #if(current_value.is_a?(Array) || current_value.class.name.include?(operator.classify))
184
+ record.send(operator) << current_value
185
+ rescue => e
186
+ logger.error e.inspect
187
+ logger.error "Cannot assign #{current_value.inspect} (#{current_value.class}) to has_many association [#{operator}] "
109
188
  end
110
189
 
111
- elsif( method_detail.operator_for(:has_one) )
190
+ elsif( current_method_detail.operator_for(:has_one) )
112
191
 
113
192
  #puts "DEBUG : HAS_MANY : #{@name} : #{operator}(#{operator_class}) - Lookup #{@current_value} in DB"
114
- if(@current_value.is_a?(method_detail.operator_class))
115
- record.send(operator + '=', @current_value)
193
+ if(current_value.is_a?(current_method_detail.operator_class))
194
+ record.send(operator + '=', current_value)
116
195
  else
117
- logger.error("ERROR #{value.class} - Not expected type for has_one #{operator} - cannot assign")
196
+ logger.error("ERROR #{current_value.class} - Not expected type for has_one #{operator} - cannot assign")
118
197
  # TODO - Not expected type - maybe try to look it up somehow ?"
119
- #insistent_has_many(record, @current_value)
120
198
  end
121
199
 
122
- elsif( method_detail.operator_for(:assignment) && method_detail.col_type )
123
- #puts "DEBUG : COl TYPE defined for #{@name} : #{@assignment} => #{@current_value} #{@col_type.type}"
124
- # puts "DEBUG : Column [#{@name}] : COl TYPE CAST: #{@current_value} => #{@col_type.type_cast( @current_value ).inspect}"
125
- record.send( operator + '=' , method_detail.col_type.type_cast( @current_value ) )
126
-
127
- #puts "DEBUG : MethodDetails Assignment RESULT: #{record.send(operator)}"
200
+ elsif( current_method_detail.operator_for(:assignment) && current_col_type)
201
+ # 'type_cast' was changed to 'type_cast_from_database'
202
+ if Rails::VERSION::STRING < '4.2.0'
203
+ logger.debug("Assign #{current_value} => [#{operator}] (CAST 2 TYPE #{current_col_type.type_cast( current_value ).inspect})")
204
+ record.send( operator + '=' , current_method_detail.col_type.type_cast( current_value ) )
205
+ else
206
+ logger.debug("Assign #{current_value} => [#{operator}] (CAST 2 TYPE #{current_col_type.type_cast_from_database( current_value ).inspect})")
207
+ record.send( operator + '=' , current_method_detail.col_type.type_cast_from_database( current_value ) )
208
+ end
128
209
 
129
- elsif( method_detail.operator_for(:assignment) )
130
- #puts "DEBUG : Column [#{@name}] : Brute force assignment of value #{@current_value}"
210
+ elsif( current_method_detail.operator_for(:assignment) )
211
+ logger.debug("Brute force assignment of value #{current_value} => [#{operator}]")
131
212
  # brute force case for assignments without a column type (which enables us to do correct type_cast)
132
213
  # so in this case, attempt straightforward assignment then if that fails, basic ops such as to_s, to_i, to_f etc
133
- insistent_assignment(record, @current_value, operator)
214
+ insistent_assignment(record, current_value, operator)
134
215
  else
135
216
  puts "WARNING: No assignment possible on #{record.inspect} using [#{operator}]"
136
217
  logger.error("WARNING: No assignment possible on #{record.inspect} using [#{operator}]")
137
218
  end
138
219
  end
139
-
220
+
140
221
  def insistent_assignment(record, value, operator)
141
-
142
- #puts "DEBUG: RECORD CLASS #{record.class}"
222
+
143
223
  op = operator + '=' unless(operator.include?('='))
144
-
224
+
145
225
  begin
146
226
  record.send(op, value)
147
227
  rescue => e
228
+
148
229
  Populator::insistent_method_list.each do |f|
149
230
  begin
150
231
  record.send(op, value.send( f) )
151
232
  break
152
233
  rescue => e
153
- puts "DEBUG: insistent_assignment: #{e.inspect}"
154
234
  if f == Populator::insistent_method_list.last
155
- puts "I'm sorry I have failed to assign [#{value}] to #{operator}"
156
- raise "I'm sorry I have failed to assign [#{value}] to #{operator}" unless value.nil?
235
+ logger.error(e.inspect)
236
+ logger.error("Failed to assign [#{value}] via operator #{operator}")
237
+ raise "Failed to assign [#{value}] to #{operator}" unless value.nil?
157
238
  end
158
239
  end
159
240
  end
160
241
  end
161
242
  end
162
-
243
+
163
244
  # Attempt to find the associated object via id, name, title ....
164
245
  def insistent_belongs_to(method_detail, record, value )
165
246
 
166
247
  operator = method_detail.operator
167
-
248
+
168
249
  if( value.class == method_detail.operator_class)
250
+ logger.info("Populator assigning #{value} to belongs_to association #{operator}")
169
251
  record.send(operator) << value
170
252
  else
171
253
 
172
- insistent_find_by_list.each do |x|
173
- begin
174
- next unless method_detail.operator_class.respond_to?( "find_by_#{x}" )
175
- item = method_detail.operator_class.send("find_by_#{x}", value)
176
- if(item)
177
- record.send(operator + '=', item)
178
- break
179
- end
180
- rescue => e
181
- logger.error("Attempt to find associated object failed for #{method_detail}")
182
- if(x == Populator::insistent_method_list.last)
183
- raise "Populator failed to assign [#{value}] via moperator #{operator}" unless value.nil?
254
+ # TODO - DRY all this
255
+ if(method_detail.find_by_operator)
256
+
257
+ item = method_detail.operator_class.where(method_detail.find_by_operator => value).first_or_create
258
+
259
+ if(item)
260
+ logger.info("Populator assigning #{item.inspect} to belongs_to association #{operator}")
261
+ record.send(operator + '=', item)
262
+ else
263
+ logger.error("Could not find or create [#{value}] for belongs_to association [#{operator}]")
264
+ raise CouldNotAssignAssociation.new "Populator failed to assign [#{value}] to belongs_to association [#{operator}]"
265
+ end
266
+
267
+ else
268
+ #try the default field names
269
+ Populator::insistent_find_by_list.each do |x|
270
+ begin
271
+
272
+ next unless method_detail.operator_class.respond_to?("where")
273
+
274
+ item = method_detail.operator_class.where(x => value).first_or_create
275
+
276
+ if(item)
277
+ logger.info("Populator assigning #{item.inspect} to belongs_to association #{operator}")
278
+ record.send(operator + '=', item)
279
+ break
280
+ end
281
+ rescue => e
282
+ logger.error(e.inspect)
283
+ logger.error("Failed attempting to find belongs_to for #{method_detail.pp}")
284
+ if(x == Populator::insistent_method_list.last)
285
+ raise CouldNotAssignAssociation.new "Populator failed to assign [#{value}] to belongs_to association [#{operator}]" unless value.nil?
286
+ end
184
287
  end
185
288
  end
186
289
  end
290
+
187
291
  end
188
292
  end
189
-
293
+
190
294
  def assignment( operator, record, value )
191
- #puts "DEBUG: RECORD CLASS #{record.class}"
295
+
192
296
  op = operator + '=' unless(operator.include?('='))
193
-
297
+
194
298
  begin
195
299
  record.send(op, value)
196
300
  rescue => e
@@ -199,7 +303,6 @@ module DataShift
199
303
  record.send(op, value.send( f) )
200
304
  break
201
305
  rescue => e
202
- #puts "DEBUG: insistent_assignment: #{e.inspect}"
203
306
  if f == Populator::insistent_method_list.last
204
307
  puts "I'm sorry I have failed to assign [#{value}] to #{operator}"
205
308
  raise "I'm sorry I have failed to assign [#{value}] to #{operator}" unless value.nil?
@@ -208,8 +311,8 @@ module DataShift
208
311
  end
209
312
  end
210
313
  end
211
-
212
-
314
+
315
+
213
316
  # Default values and over rides can be provided in Ruby/YAML ???? config file.
214
317
  #
215
318
  # Format :
@@ -224,80 +327,93 @@ module DataShift
224
327
  #
225
328
  def configure_from(load_object_class, yaml_file)
226
329
 
227
- data = YAML::load( File.open(yaml_file) )
228
-
229
- # TODO - MOVE DEFAULTS TO OWN MODULE
230
- # decorate the loading class with the defaults/ove rides to manage itself
231
- # IDEAS .....
232
- #
233
- #unless(@default_data_objects[load_object_class])
234
- #
235
- # @default_data_objects[load_object_class] = load_object_class.new
236
-
237
- # default_data_object = @default_data_objects[load_object_class]
238
-
239
-
240
- # default_data_object.instance_eval do
241
- # def datashift_defaults=(hash)
242
- # @datashift_defaults = hash
243
- # end
244
- # def datashift_defaults
245
- # @datashift_defaults
246
- # end
247
- #end unless load_object_class.respond_to?(:datashift_defaults)
248
- #end
249
-
250
- #puts load_object_class.new.to_yaml
251
-
252
- logger.info("Read Datashift loading config: #{data.inspect}")
253
-
330
+ data = YAML::load( ERB.new( IO.read(yaml_file) ).result )
331
+
332
+ # TODO - MOVE DEFAULTS TO OWN MODULE
333
+
334
+ logger.info("Setting Populator defaults: #{data.inspect}")
335
+
254
336
  if(data[load_object_class.name])
255
-
256
- logger.info("Assigning defaults and over rides from config")
257
-
337
+
258
338
  deflts = data[load_object_class.name]['datashift_defaults']
259
339
  default_values.merge!(deflts) if deflts
260
-
340
+
341
+ logger.info("Set Populator default_values: #{default_values.inspect}")
342
+
261
343
  ovrides = data[load_object_class.name]['datashift_overrides']
262
344
  override_values.merge!(ovrides) if ovrides
345
+ logger.info("Set Populator overrides: #{override_values.inspect}")
346
+
347
+ subs = data[load_object_class.name]['datashift_substitutions']
348
+
349
+ subs.each do |o, sub|
350
+ # TODO support single array as well as multiple [[..,..], [....]]
351
+ sub.each { |tuple| set_substitution(o, tuple) }
352
+ end if(subs)
353
+
263
354
  end
264
-
265
355
 
266
356
  end
267
-
357
+
358
+ # Set a default value to be used to populate Model.operator
359
+ # Generally defaults will be used when no value supplied.
360
+ def set_substitution(operator, value )
361
+ substitutions[operator] ||= []
362
+
363
+ substitutions[operator] << Struct::Substitution.new(value[0], value[1])
364
+ end
365
+
366
+ def substitutions
367
+ @substitutions ||= {}
368
+ end
369
+
370
+ def substitute( operator )
371
+ subs = substitutions[operator] || {}
372
+
373
+ subs.each do |s|
374
+ @original_value_before_override = @current_value
375
+ @current_value = @original_value_before_override.gsub(s.pattern.to_s, s.replacement.to_s)
376
+ end
377
+ end
378
+
379
+
268
380
  # Set a value to be used to populate Model.operator
269
381
  # Generally over-rides will be used regardless of what value caller supplied.
270
382
  def set_override_value( operator, value )
271
383
  override_values[operator] = value
272
384
  end
273
-
385
+
274
386
  def override_values
275
387
  @override_values ||= {}
276
388
  end
277
-
389
+
278
390
  def override_value( operator )
279
391
  if(override_values[operator])
280
392
  @original_value_before_override = @current_value
281
-
393
+
282
394
  @current_value = @override_values[operator]
283
395
  end
284
396
  end
285
-
397
+
398
+
399
+ def has_override?( operator )
400
+ return override_values.has_key?(operator)
401
+ end
402
+
286
403
  # Set a default value to be used to populate Model.operator
287
404
  # Generally defaults will be used when no value supplied.
288
405
  def set_default_value(operator, value )
289
406
  default_values[operator] = value
290
407
  end
291
-
408
+
292
409
  def default_values
293
410
  @default_values ||= {}
294
411
  end
295
-
412
+
296
413
  # Return the default value for supplied operator
297
414
  def default_value(operator)
298
415
  default_values[operator]
299
416
  end
300
-
301
417
 
302
418
  def set_prefix( operator, value )
303
419
  prefixes[operator] = value
@@ -310,7 +426,7 @@ module DataShift
310
426
  def prefixes
311
427
  @prefixes ||= {}
312
428
  end
313
-
429
+
314
430
  def set_postfix(operator, value )
315
431
  postfixes[operator] = value
316
432
  end
@@ -318,12 +434,11 @@ module DataShift
318
434
  def postfix(operator)
319
435
  postfixes[operator]
320
436
  end
321
-
437
+
322
438
  def postfixes
323
439
  @postfixes ||= {}
324
440
  end
325
-
326
-
441
+
327
442
  end
328
-
443
+
329
444
  end