datashift 0.13.0 → 0.14.0

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