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.
- 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
|
|