datashift 0.13.0 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +36 -66
- data/VERSION +1 -1
- data/lib/applications/jexcel_file.rb +12 -5
- data/lib/datashift.rb +18 -13
- data/lib/datashift/delimiters.rb +0 -1
- data/lib/{guards.rb → datashift/guards.rb} +0 -0
- data/lib/datashift/method_detail.rb +4 -67
- data/lib/datashift/method_details_manager.rb +18 -6
- data/lib/datashift/method_dictionary.rb +55 -38
- data/lib/datashift/method_mapper.rb +18 -14
- data/lib/datashift/populator.rb +259 -6
- data/lib/exporters/csv_exporter.rb +28 -19
- data/lib/exporters/excel_exporter.rb +18 -5
- data/lib/generators/excel_generator.rb +2 -0
- data/lib/loaders/excel_loader.rb +2 -1
- data/lib/loaders/loader_base.rb +53 -142
- data/lib/loaders/paperclip/attachment_loader.rb +1 -1
- data/lib/loaders/paperclip/datashift_paperclip.rb +51 -44
- data/lib/thor/export.thor +65 -0
- data/lib/thor/generate.thor +68 -4
- data/spec/Gemfile +12 -8
- data/spec/Gemfile.lock +93 -93
- data/spec/csv_exporter_spec.rb +50 -12
- data/spec/excel_exporter_spec.rb +35 -3
- data/spec/excel_loader_spec.rb +9 -7
- data/spec/excel_spec.rb +26 -5
- data/spec/{loader_spec.rb → loader_base_spec.rb} +13 -1
- data/spec/method_dictionary_spec.rb +77 -70
- data/spec/paperclip_loader_spec.rb +1 -1
- data/spec/populator_spec.rb +94 -0
- data/spec/thor_spec.rb +1 -1
- metadata +70 -68
@@ -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(
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
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
|
|
data/lib/datashift/populator.rb
CHANGED
@@ -3,25 +3,137 @@
|
|
3
3
|
# Date :: March 2012
|
4
4
|
# License:: MIT
|
5
5
|
#
|
6
|
-
# Details::
|
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
|
-
|
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
|
-
|
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(
|
37
|
+
def export(export_records, options = {})
|
38
|
+
|
39
|
+
records = [*export_records]
|
37
40
|
|
38
|
-
|
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
|
-
|
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
|
-
|
130
|
+
# details_mgr.get_operators(op_type).each do |operator|
|
122
131
|
assoc_operators.each do |operator|
|
123
|
-
|
132
|
+
assoc_object = r.send(operator)
|
124
133
|
|
125
|
-
|
126
|
-
|
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
|
-
|
131
|
-
|
139
|
+
csv << "#{@text_delim}#{column_text}#{@text_delim}" << Delimiters::csv_delim
|
140
|
+
#csv << record_to_csv(r)
|
132
141
|
|
133
|
-
|
134
|
-
|
142
|
+
elsif(assoc_object.is_a? Array)
|
143
|
+
items_to_s = assoc_object.collect {|x| record_to_column(x) }
|
135
144
|
|
136
|
-
|
137
|
-
|
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
|
-
|
140
|
-
|
141
|
-
|
148
|
+
else
|
149
|
+
csv << Delimiters::csv_delim
|
150
|
+
end
|
142
151
|
#end
|
143
152
|
end
|
144
153
|
|