datashift 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,9 +15,6 @@
15
15
  #
16
16
  # This real association can then be used to send spreadsheet row data to the AR object.
17
17
  #
18
- require 'method_detail'
19
- require 'method_dictionary'
20
-
21
18
  module DataShift
22
19
 
23
20
  class MethodMapper
@@ -84,6 +81,8 @@ module DataShift
84
81
  DataShift::MethodDictionary.build_method_details(klass)
85
82
  end
86
83
 
84
+ mgr = DataShift::MethodDictionary.method_details_mgrs[klass]
85
+
87
86
  forced = [*options[:force_inclusion]].compact.collect { |f| f.to_s.downcase }
88
87
 
89
88
  @method_details, @missing_methods = [], []
@@ -99,26 +98,29 @@ module DataShift
99
98
  end
100
99
 
101
100
  raw_col_name, lookup = raw_col_data.split(MethodMapper::column_delim)
102
-
103
- md = MethodDictionary::find_method_detail( klass, raw_col_name )
104
-
105
- # TODO be nice if we could check that the assoc on klass responds to the specified
106
- # lookup key now (nice n early)
107
- # active_record_helper = "find_by_#{lookup}"
108
- if(md.nil? && (options[:include_all] || forced.include?(raw_col_name.downcase)) )
109
- md = MethodDictionary::add(klass, raw_col_name)
101
+
102
+ md = MethodDictionary::find_method_detail(klass, raw_col_name)
103
+
104
+ if(md.nil?)
105
+ #puts "DEBUG: Check Forced\n #{forced}.include?(#{raw_col_name}) #{forced.include?(raw_col_name.downcase)}"
106
+
107
+ if(options[:include_all] || forced.include?(raw_col_name.downcase))
108
+ md = MethodDictionary::add(klass, raw_col_name)
109
+ end
110
110
  end
111
111
 
112
- if(md)
113
-
112
+ if(md)
114
113
  md.name = raw_col_name
115
114
  md.column_index = col_index
116
115
 
116
+ # TODO we should check that the assoc on klass responds to the specified
117
+ # lookup key now (nice n early)
118
+ # active_record_helper = "find_by_#{lookup}"
117
119
  if(lookup)
118
120
  find_by, find_value = lookup.split(MethodMapper::column_delim)
119
121
  md.find_by_value = find_value
120
122
  md.find_by_operator = find_by # TODO and klass.x.respond_to?(active_record_helper))
121
- #puts "DEBUG: Method Detail #{md.name};#{md.operator} : find_by_operator #{md.find_by_operator}"
123
+ puts "DEBUG: Method Detail #{md.name};#{md.operator} : find_by_operator #{md.find_by_operator}"
122
124
  end
123
125
  else
124
126
  # TODO populate unmapped with a real MethodDetail that is 'null' and create is_nil
@@ -149,7 +151,9 @@ module DataShift
149
151
  # Returns true if discovered methods contain every operator in mandatory_list
150
152
  def contains_mandatory?( mandatory_list )
151
153
  a = [*mandatory_list].collect { |f| f.downcase }
154
+ puts a.inspect
152
155
  b = operator_names.collect { |f| f.downcase }
156
+ puts b.inspect
153
157
  (a - b).empty?
154
158
  end
155
159
 
@@ -3,25 +3,137 @@
3
3
  # Date :: March 2012
4
4
  # License:: MIT
5
5
  #
6
- # Details:: This modules provides individual population methods on an AR model.
6
+ # Details:: The default Populator class for assigning data to models
7
+ #
8
+ # Provides individual population methods on an AR model.
7
9
  #
8
10
  # Enables users to assign values to AR object, without knowing much about that receiving object.
9
11
  #
10
12
  require 'to_b'
13
+ require 'logging'
11
14
 
12
15
  module DataShift
13
16
 
14
- module Populator
17
+ class Populator
15
18
 
19
+ include DataShift::Logging
20
+
16
21
  def self.insistent_method_list
17
22
  @insistent_method_list ||= [:to_s, :to_i, :to_f, :to_b]
18
- @insistent_method_list
19
23
  end
20
24
 
21
- def self.insistent_assignment( record, value, operator )
25
+ # When looking up an association, when no field provided, try each of these in turn till a match
26
+ # i.e find_by_name, find_by_title, find_by_id
27
+ def self.insistent_find_by_list
28
+ @insistent_find_by_list ||= [:name, :title, :id]
29
+ end
30
+
31
+
32
+ attr_reader :current_value, :original_value_before_override
33
+ attr_reader :current_attribute_hash
34
+ attr_reader :current_method_detail
35
+
36
+ def initialize
37
+ @current_value = nil
38
+ @original_value_before_override = nil
39
+ @current_attribute_hash = {}
40
+
41
+ end
42
+
43
+ # Set member variables to hold details, value and optional attributes.
44
+ #
45
+ # Check supplied value, validate it, and if required :
46
+ # set to provided default value
47
+ # prepend any provided prefixes
48
+ # add any provided postfixes
49
+ 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)
69
+ end
70
+
71
+ @current_value = "#{prefix(operator)}#{@current_value}" if(prefix(operator))
72
+ @current_value = "#{@current_value}#{postfix(operator)}" if(postfix(operator))
73
+
74
+ return @current_value, @current_attribute_hash
75
+ end
76
+
77
+ def assign(method_detail, record, value )
78
+
79
+ @current_value = value
80
+
81
+ # logger.info("WARNING nil value supplied for Column [#{@name}]") if(@current_value.nil?)
82
+
83
+ operator = method_detail.operator
84
+
85
+ if( method_detail.operator_for(:belongs_to) )
86
+
87
+ #puts "DEBUG : BELONGS_TO : #{@name} : #{operator} - Lookup #{@current_value} in DB"
88
+ insistent_belongs_to(method_detail, record, @current_value)
89
+
90
+ elsif( method_detail.operator_for(:has_many) )
91
+
92
+ #puts "DEBUG : VALUE TYPE [#{value.class.name.include?(operator.classify)}] [#{ModelMapper.class_from_string(value.class.name)}]" unless(value.is_a?(Array))
93
+
94
+ # The include? check is best I can come up with right now .. to handle module/namespaces
95
+ # TODO - can we determine the real class type of an association
96
+ # e.g given a association taxons, which operator.classify gives us Taxon, but actually it's Spree::Taxon
97
+ # so how do we get from 'taxons' to Spree::Taxons ? .. check if further info in reflect_on_all_associations
98
+
99
+ if(value.is_a?(Array) || value.class.name.include?(operator.classify))
100
+ record.send(operator) << value
101
+ else
102
+ puts "ERROR #{value.class} - Not expected type for has_many #{operator} - cannot assign"
103
+ end
104
+
105
+ elsif( method_detail.operator_for(:has_one) )
106
+
107
+ #puts "DEBUG : HAS_MANY : #{@name} : #{operator}(#{operator_class}) - Lookup #{@current_value} in DB"
108
+ if(value.is_a?(method_detail.operator_class))
109
+ record.send(operator + '=', value)
110
+ else
111
+ logger.error("ERROR #{value.class} - Not expected type for has_one #{operator} - cannot assign")
112
+ # TODO - Not expected type - maybe try to look it up somehow ?"
113
+ #insistent_has_many(record, @current_value)
114
+ end
115
+
116
+ elsif( method_detail.operator_for(:assignment) && method_detail.col_type )
117
+ #puts "DEBUG : COl TYPE defined for #{@name} : #{@assignment} => #{@current_value} #{@col_type.type}"
118
+ # puts "DEBUG : Column [#{@name}] : COl TYPE CAST: #{@current_value} => #{@col_type.type_cast( @current_value ).inspect}"
119
+ record.send( operator + '=' , method_detail.col_type.type_cast( @current_value ) )
120
+
121
+ #puts "DEBUG : MethodDetails Assignment RESULT: #{record.send(operator)}"
122
+
123
+ elsif( method_detail.operator_for(:assignment) )
124
+ #puts "DEBUG : Column [#{@name}] : Brute force assignment of value #{@current_value}"
125
+ # brute force case for assignments without a column type (which enables us to do correct type_cast)
126
+ # so in this case, attempt straightforward assignment then if that fails, basic ops such as to_s, to_i, to_f etc
127
+ insistent_assignment(record, @current_value, operator)
128
+ else
129
+ puts "WARNING: No assignment possible on #{record.inspect} using [#{operator}]"
130
+ end
131
+ end
132
+
133
+ def insistent_assignment(record, value, operator)
22
134
 
23
135
  #puts "DEBUG: RECORD CLASS #{record.class}"
24
- op = operator + '='
136
+ op = operator + '=' unless(operator.include?('='))
25
137
 
26
138
  begin
27
139
  record.send(op, value)
@@ -41,6 +153,33 @@ module DataShift
41
153
  end
42
154
  end
43
155
 
156
+ # Attempt to find the associated object via id, name, title ....
157
+ def insistent_belongs_to(method_detail, record, value )
158
+
159
+ operator = method_detail.operator
160
+
161
+ if( value.class == method_detail.operator_class)
162
+ record.send(operator) << value
163
+ else
164
+
165
+ insistent_find_by_list.each do |x|
166
+ begin
167
+ next unless method_detail.operator_class.respond_to?( "find_by_#{x}" )
168
+ item = method_detail.operator_class.send("find_by_#{x}", value)
169
+ if(item)
170
+ record.send(operator + '=', item)
171
+ break
172
+ end
173
+ rescue => e
174
+ puts "ERROR: #{e.inspect}"
175
+ if(x == Populator::insistent_method_list.last)
176
+ raise "Populator failed to assign [#{value}] via moperator #{operator}" unless value.nil?
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+
44
183
  def assignment( operator, record, value )
45
184
  #puts "DEBUG: RECORD CLASS #{record.class}"
46
185
  op = operator + '=' unless(operator.include?('='))
@@ -64,6 +203,120 @@ module DataShift
64
203
  end
65
204
 
66
205
 
206
+ # Default values and over rides can be provided in Ruby/YAML ???? config file.
207
+ #
208
+ # Format :
209
+ #
210
+ # Load Class: (e.g Spree:Product)
211
+ # datashift_defaults:
212
+ # value_as_string: "Default Project Value"
213
+ # category: reference:category_002
214
+ #
215
+ # datashift_overrides:
216
+ # value_as_double: 99.23546
217
+ #
218
+ def configure_from(load_object_class, yaml_file)
219
+
220
+ data = YAML::load( File.open(yaml_file) )
221
+
222
+ # TODO - MOVE DEFAULTS TO OWN MODULE
223
+ # decorate the loading class with the defaults/ove rides to manage itself
224
+ # IDEAS .....
225
+ #
226
+ #unless(@default_data_objects[load_object_class])
227
+ #
228
+ # @default_data_objects[load_object_class] = load_object_class.new
229
+
230
+ # default_data_object = @default_data_objects[load_object_class]
231
+
232
+
233
+ # default_data_object.instance_eval do
234
+ # def datashift_defaults=(hash)
235
+ # @datashift_defaults = hash
236
+ # end
237
+ # def datashift_defaults
238
+ # @datashift_defaults
239
+ # end
240
+ #end unless load_object_class.respond_to?(:datashift_defaults)
241
+ #end
242
+
243
+ #puts load_object_class.new.to_yaml
244
+
245
+ logger.info("Read Datashift loading config: #{data.inspect}")
246
+
247
+ if(data[load_object_class.name])
248
+
249
+ logger.info("Assigning defaults and over rides from config")
250
+
251
+ deflts = data[load_object_class.name]['datashift_defaults']
252
+ default_values.merge!(deflts) if deflts
253
+
254
+ ovrides = data[load_object_class.name]['datashift_overrides']
255
+ override_values.merge!(ovrides) if ovrides
256
+ end
257
+
258
+
259
+ end
260
+
261
+ # Set a value to be used to populate Model.operator
262
+ # Generally over-rides will be used regardless of what value caller supplied.
263
+ def set_override_value( operator, value )
264
+ override_values[operator] = value
265
+ end
266
+
267
+ def override_values
268
+ @override_values ||= {}
269
+ end
270
+
271
+ def override_value( operator )
272
+ if(override_values[operator])
273
+ @original_value_before_override = @current_value
274
+
275
+ @current_value = @override_values[operator]
276
+ end
277
+ end
278
+
279
+ # Set a default value to be used to populate Model.operator
280
+ # Generally defaults will be used when no value supplied.
281
+ def set_default_value(operator, value )
282
+ default_values[operator] = value
283
+ end
284
+
285
+ def default_values
286
+ @default_values ||= {}
287
+ end
288
+
289
+ # Return the default value for supplied operator
290
+ def default_value(operator)
291
+ default_values[operator]
292
+ end
293
+
294
+
295
+ def set_prefix( operator, value )
296
+ prefixes[operator] = value
297
+ end
298
+
299
+ def prefix(operator)
300
+ prefixes[operator]
301
+ end
302
+
303
+ def prefixes
304
+ @prefixes ||= {}
305
+ end
306
+
307
+ def set_postfix(operator, value )
308
+ postfixes[operator] = value
309
+ end
310
+
311
+ def postfix(operator)
312
+ postfixes[operator]
313
+ end
314
+
315
+ def postfixes
316
+ @postfixes ||= {}
317
+ end
318
+
319
+
67
320
  end
68
321
 
69
- end
322
+ end
@@ -12,7 +12,9 @@ require 'csv'
12
12
  module DataShift
13
13
 
14
14
  class CsvExporter < ExporterBase
15
-
15
+
16
+ include DataShift::Logging
17
+
16
18
  attr_accessor :text_delim
17
19
 
18
20
  def initialize(filename)
@@ -20,7 +22,6 @@ module DataShift
20
22
  @text_delim = "\'"
21
23
  end
22
24
 
23
-
24
25
  # Return opposite of text delim - "hello, 'barry'" => '"hello, "barry""'
25
26
  def escape_text_delim
26
27
  return '"' if @text_delim == "\'"
@@ -33,14 +34,22 @@ module DataShift
33
34
  # => :text_delim => Char to use to delim columns, useful when data contain embedded ','
34
35
  # => ::methods => List of methods to additionally call on each record
35
36
  #
36
- def export(records, options = {})
37
+ def export(export_records, options = {})
38
+
39
+ records = [*export_records]
37
40
 
38
- raise ArgumentError.new('Please supply array of records to export') unless records.is_a? Array
41
+
42
+ puts records, records.inspect
43
+
44
+ unless(records && records.size > 0)
45
+ logger.warn("No objects supplied for export")
46
+ return
47
+ end
39
48
 
40
49
  first = records[0]
41
50
 
42
- return unless(first.is_a?(ActiveRecord::Base))
43
-
51
+ raise ArgumentError.new('Please supply set of ActiveRecord objects to export') unless(first.is_a?(ActiveRecord::Base))
52
+
44
53
  f = options[:filename] || filename()
45
54
 
46
55
  @text_delim = options[:text_delim] if(options[:text_delim])
@@ -118,27 +127,27 @@ module DataShift
118
127
  # done records basic attributes now deal with associations
119
128
 
120
129
  #assoc_work_list.each do |op_type|
121
- # details_mgr.get_operators(op_type).each do |operator|
130
+ # details_mgr.get_operators(op_type).each do |operator|
122
131
  assoc_operators.each do |operator|
123
- assoc_object = r.send(operator)
132
+ assoc_object = r.send(operator)
124
133
 
125
- if(assoc_object.is_a?ActiveRecord::Base)
126
- column_text = record_to_column(assoc_object) # belongs_to or has_one
134
+ if(assoc_object.is_a?ActiveRecord::Base)
135
+ column_text = record_to_column(assoc_object) # belongs_to or has_one
127
136
 
128
137
  # TODO -ColumnPacker class shared between excel/csv
129
138
 
130
- csv << "#{@text_delim}#{column_text}#{@text_delim}" << Delimiters::csv_delim
131
- #csv << record_to_csv(r)
139
+ csv << "#{@text_delim}#{column_text}#{@text_delim}" << Delimiters::csv_delim
140
+ #csv << record_to_csv(r)
132
141
 
133
- elsif(assoc_object.is_a? Array)
134
- items_to_s = assoc_object.collect {|x| record_to_column(x) }
142
+ elsif(assoc_object.is_a? Array)
143
+ items_to_s = assoc_object.collect {|x| record_to_column(x) }
135
144
 
136
- # create a single column
137
- csv << "#{@text_delim}#{items_to_s.join(Delimiters::multi_assoc_delim)}#{@text_delim}" << Delimiters::csv_delim
145
+ # create a single column
146
+ csv << "#{@text_delim}#{items_to_s.join(Delimiters::multi_assoc_delim)}#{@text_delim}" << Delimiters::csv_delim
138
147
 
139
- else
140
- csv << Delimiters::csv_delim
141
- end
148
+ else
149
+ csv << Delimiters::csv_delim
150
+ end
142
151
  #end
143
152
  end
144
153