datashift 0.2.2 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +15 -3
- data/VERSION +1 -1
- data/datashift.gemspec +11 -3
- data/lib/applications/jruby/jexcel_file.rb +10 -3
- data/lib/datashift.rb +25 -62
- data/lib/datashift/exceptions.rb +1 -0
- data/lib/datashift/logging.rb +58 -0
- data/lib/datashift/method_detail.rb +6 -45
- data/lib/datashift/method_details_manager.rb +54 -0
- data/lib/datashift/method_dictionary.rb +6 -1
- data/lib/datashift/method_mapper.rb +12 -5
- data/lib/datashift/populator.rb +46 -0
- data/lib/exporters/excel_exporter.rb +1 -1
- data/lib/generators/excel_generator.rb +48 -44
- data/lib/helpers/spree_helper.rb +14 -3
- data/lib/loaders/csv_loader.rb +9 -6
- data/lib/loaders/excel_loader.rb +5 -1
- data/lib/loaders/loader_base.rb +28 -14
- data/lib/loaders/spree/image_loader.rb +36 -34
- data/lib/loaders/spree/product_loader.rb +17 -7
- data/lib/thor/generate_excel.thor +35 -15
- data/lib/thor/import_excel.thor +84 -0
- data/lib/thor/spree/bootstrap_cleanup.thor +33 -0
- data/lib/thor/spree/products_images.thor +171 -0
- data/spec/datashift_spec.rb +19 -0
- data/spec/excel_exporter_spec.rb +3 -3
- data/spec/fixtures/datashift_Spree_db.sqlite +0 -0
- data/spec/fixtures/datashift_test_models_db.sqlite +0 -0
- data/spec/fixtures/spree/SpreeProductsDefaults.yml +15 -0
- data/spec/fixtures/spree/SpreeProductsMandatoryOnly.xls +0 -0
- data/spec/fixtures/spree/SpreeProductsWithImages.xls +0 -0
- data/spec/spec_helper.rb +2 -2
- data/spec/spree_generator_spec.rb +14 -0
- data/spec/spree_images_loader_spec.rb +73 -0
- data/spec/spree_loader_spec.rb +53 -19
- data/tasks/spree/image_load.rake +18 -13
- metadata +11 -3
- data/tasks/spree/product_loader.rake +0 -44
@@ -11,6 +11,8 @@ module DataShift
|
|
11
11
|
|
12
12
|
class MethodDictionary
|
13
13
|
|
14
|
+
include DataShift::Logging
|
15
|
+
|
14
16
|
def initialize
|
15
17
|
end
|
16
18
|
|
@@ -22,12 +24,15 @@ module DataShift
|
|
22
24
|
# :instance_methods => if true include instance method type assignment operators as well as model's pure columns
|
23
25
|
#
|
24
26
|
def self.find_operators(klass, options = {} )
|
27
|
+
|
28
|
+
raise "Cannot find operators supplied klass nil #{klass}" if(klass.nil?)
|
25
29
|
|
26
30
|
# Find the has_many associations which can be populated via <<
|
27
31
|
if( options[:reload] || has_many[klass].nil? )
|
28
32
|
has_many[klass] = klass.reflect_on_all_associations(:has_many).map { |i| i.name.to_s }
|
29
33
|
klass.reflect_on_all_associations(:has_and_belongs_to_many).inject(has_many[klass]) { |x,i| x << i.name.to_s }
|
30
34
|
end
|
35
|
+
|
31
36
|
# puts "DEBUG: Has Many Associations:", has_many[klass].inspect
|
32
37
|
|
33
38
|
# Find the belongs_to associations which can be populated via Model.belongs_to_name = OtherArModelObject
|
@@ -48,7 +53,7 @@ module DataShift
|
|
48
53
|
# Note, not all reflections return method names in same style so we convert all to
|
49
54
|
# the raw form i.e without the '=' for consistency
|
50
55
|
if( options[:reload] || assignments[klass].nil? )
|
51
|
-
|
56
|
+
|
52
57
|
assignments[klass] = klass.column_names
|
53
58
|
|
54
59
|
if(options[:instance_methods] == true)
|
@@ -22,6 +22,8 @@ module DataShift
|
|
22
22
|
|
23
23
|
class MethodMapper
|
24
24
|
|
25
|
+
include DataShift::Logging
|
26
|
+
|
25
27
|
attr_accessor :header_row, :headers
|
26
28
|
attr_accessor :method_details, :missing_methods
|
27
29
|
|
@@ -43,16 +45,21 @@ module DataShift
|
|
43
45
|
@headers = []
|
44
46
|
end
|
45
47
|
|
46
|
-
# Build complete picture of the methods whose names listed in
|
47
|
-
# Handles method names as defined by a user or
|
48
|
-
# not be exactly as required e.g handles capitalisation, white space, _ etc
|
48
|
+
# Build complete picture of the methods whose names listed in columns
|
49
|
+
# Handles method names as defined by a user, from spreadsheets or file headers where the names
|
50
|
+
# specified may not be exactly as required e.g handles capitalisation, white space, _ etc
|
49
51
|
# Returns: Array of matching method_details
|
50
52
|
#
|
51
|
-
def map_inbound_to_methods( klass,
|
53
|
+
def map_inbound_to_methods( klass, columns )
|
52
54
|
|
53
55
|
@method_details, @missing_methods = [], []
|
54
56
|
|
55
|
-
|
57
|
+
columns.each do |name|
|
58
|
+
if(name.nil? or name.empty?)
|
59
|
+
logger.warn("Column list contains empty or null columns")
|
60
|
+
next
|
61
|
+
end
|
62
|
+
|
56
63
|
x, lookup = name.split(MethodMapper::column_delim)
|
57
64
|
md = MethodDictionary::find_method_detail( klass, x )
|
58
65
|
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Copyright:: (c) Autotelik Media Ltd 2012
|
2
|
+
# Author :: Tom Statter
|
3
|
+
# Date :: March 2012
|
4
|
+
# License:: MIT
|
5
|
+
#
|
6
|
+
# Details:: This modules provides individual population methods on an AR model.
|
7
|
+
#
|
8
|
+
# Enables users to assign values to AR object, without knowing much about that receiving object.
|
9
|
+
#
|
10
|
+
require 'to_b'
|
11
|
+
|
12
|
+
module DataShift
|
13
|
+
|
14
|
+
module Populator
|
15
|
+
|
16
|
+
def self.insistent_method_list
|
17
|
+
@insistent_method_list ||= [:to_s, :to_i, :to_f, :to_b]
|
18
|
+
@insistent_method_list
|
19
|
+
end
|
20
|
+
|
21
|
+
def assignment( operator, record, value )
|
22
|
+
#puts "DEBUG: RECORD CLASS #{record.class}"
|
23
|
+
op = operator + '=' unless(operator.include?('='))
|
24
|
+
|
25
|
+
begin
|
26
|
+
record.send(op, value)
|
27
|
+
rescue => e
|
28
|
+
Populator::insistent_method_list.each do |f|
|
29
|
+
begin
|
30
|
+
record.send(op, value.send( f) )
|
31
|
+
break
|
32
|
+
rescue => e
|
33
|
+
#puts "DEBUG: insistent_assignment: #{e.inspect}"
|
34
|
+
if f == Populator::insistent_method_list.last
|
35
|
+
puts "I'm sorry I have failed to assign [#{value}] to #{operator}"
|
36
|
+
raise "I'm sorry I have failed to assign [#{value}] to #{operator}" unless value.nil?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -108,7 +108,7 @@ module DataShift
|
|
108
108
|
|
109
109
|
def initialize(filename)
|
110
110
|
@filename = filename
|
111
|
-
raise DataShift::BadRuby, "Apologies but
|
111
|
+
raise DataShift::BadRuby, "Apologies but DataShift Excel facilities currently need JRuby. Please switch to, or install JRuby"
|
112
112
|
end
|
113
113
|
end
|
114
114
|
end # jruby
|
@@ -18,7 +18,9 @@ module DataShift
|
|
18
18
|
|
19
19
|
class ExcelGenerator < GeneratorBase
|
20
20
|
|
21
|
-
|
21
|
+
include DataShift::Logging
|
22
|
+
|
23
|
+
attr_accessor :excel, :filename
|
22
24
|
|
23
25
|
def initialize(filename)
|
24
26
|
@filename = filename
|
@@ -27,70 +29,72 @@ module DataShift
|
|
27
29
|
# Create an Excel file template (header row) representing supplied Model
|
28
30
|
|
29
31
|
def generate(klass, options = {})
|
30
|
-
|
31
|
-
|
32
|
-
@filename = options[:filename] if options[:filename]
|
33
|
-
|
34
|
-
excel = JExcelFile.new()
|
35
|
-
|
36
|
-
if(options[:sheet_name] )
|
37
|
-
excel.create_sheet( options[:sheet_name] )
|
38
|
-
else
|
39
|
-
excel.create_sheet( klass.name )
|
40
|
-
end
|
32
|
+
|
33
|
+
prepare(klass, options)
|
41
34
|
|
42
|
-
|
35
|
+
@excel.set_headers(MethodDictionary.assignments[klass])
|
43
36
|
|
44
|
-
|
45
|
-
|
46
|
-
excel.save( @filename )
|
37
|
+
logger.info("ExcelGenerator saving generated template #{@filename}")
|
38
|
+
|
39
|
+
@excel.save( @filename )
|
47
40
|
end
|
48
41
|
|
49
42
|
|
50
43
|
# Create an Excel file from list of ActiveRecord objects
|
51
|
-
#
|
52
|
-
#
|
44
|
+
# To remove type(s) of associations specify option :
|
45
|
+
# :exclude => [type(s)]
|
46
|
+
#
|
47
|
+
# Possible values are given by MethodDetail::supported_types_enum
|
48
|
+
# ... [:assignment, :belongs_to, :has_one, :has_many]
|
53
49
|
#
|
50
|
+
# Options
|
54
51
|
def generate_with_associations(klass, options = {})
|
55
52
|
|
56
|
-
|
57
|
-
|
58
|
-
if(options[:sheet_name] )
|
59
|
-
excel.create_sheet( options[:sheet_name] )
|
60
|
-
else
|
61
|
-
excel.create_sheet( klass.name )
|
62
|
-
end
|
63
|
-
|
64
|
-
MethodDictionary.find_operators( klass )
|
53
|
+
prepare(klass, options)
|
65
54
|
|
66
55
|
MethodDictionary.build_method_details( klass )
|
67
56
|
|
68
|
-
work_list =
|
69
|
-
|
57
|
+
work_list = MethodDetail::supported_types_enum.to_a
|
58
|
+
work_list -= options[:exclude].to_a
|
59
|
+
|
70
60
|
headers = []
|
71
|
-
puts "work_list : [#{work_list.inspect}]"
|
72
61
|
|
73
62
|
details_mgr = MethodDictionary.method_details_mgrs[klass]
|
74
63
|
|
75
64
|
work_list.each do |op_type|
|
76
65
|
list_for_class_and_op = details_mgr.get_list(op_type)
|
77
|
-
|
66
|
+
|
78
67
|
next if(list_for_class_and_op.nil? || list_for_class_and_op.empty?)
|
79
|
-
|
80
|
-
#each do |mdtype|
|
81
|
-
#end
|
82
|
-
#if(MethodDictionary.respond_to?("#{mdtype}_for") )
|
83
|
-
# method_details = MethodDictionary.send("#{mdtype}_for", klass)
|
84
|
-
|
85
|
-
list_for_class_and_op.each {|md| headers << "#{md.operator}" }
|
86
|
-
#else
|
87
|
-
# puts "ERROR : Unknown option in :with [#{mdtype}]"
|
88
|
-
|
68
|
+
list_for_class_and_op.each {|md| headers << "#{md.operator}" }
|
89
69
|
end
|
90
70
|
|
91
|
-
excel.set_headers( headers )
|
71
|
+
@excel.set_headers( headers )
|
92
72
|
|
93
|
-
excel.save( filename() )
|
73
|
+
@excel.save( filename() )
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def prepare(klass, options = {})
|
79
|
+
@filename = options[:filename] if options[:filename]
|
80
|
+
|
81
|
+
logger.info("ExcelGenerator creating template with associations for class #{klass}")
|
82
|
+
|
83
|
+
@excel = JExcelFile.new()
|
84
|
+
|
85
|
+
if(options[:sheet_name] )
|
86
|
+
@excel.create_sheet( options[:sheet_name] )
|
87
|
+
else
|
88
|
+
@excel.create_sheet( klass.name )
|
89
|
+
end
|
90
|
+
|
91
|
+
unless @excel.sheet
|
92
|
+
logger.error("Excel failed to create WorkSheet for #{klass.name}")
|
93
|
+
|
94
|
+
raise "Failed to create Excel WorkSheet for #{klass.name}"
|
95
|
+
end
|
96
|
+
|
97
|
+
MethodDictionary.find_operators( klass )
|
94
98
|
end
|
95
99
|
end # ExcelGenerator
|
96
100
|
|
@@ -99,7 +103,7 @@ module DataShift
|
|
99
103
|
|
100
104
|
def initialize(filename)
|
101
105
|
@filename = filename
|
102
|
-
raise DataShift::BadRuby, "Apologies but
|
106
|
+
raise DataShift::BadRuby, "Apologies but DataShift Excel facilities currently need JRuby. Please switch to, or install JRuby"
|
103
107
|
end
|
104
108
|
end
|
105
109
|
end # jruby
|
data/lib/helpers/spree_helper.rb
CHANGED
@@ -25,8 +25,6 @@
|
|
25
25
|
# as the database is auto generated
|
26
26
|
# =>
|
27
27
|
|
28
|
-
|
29
|
-
|
30
28
|
module DataShift
|
31
29
|
|
32
30
|
module SpreeHelper
|
@@ -72,10 +70,15 @@ module DataShift
|
|
72
70
|
end
|
73
71
|
|
74
72
|
|
75
|
-
#
|
73
|
+
# Datashift is usually included and tasks pulled in by a parent/host application.
|
76
74
|
# So here we are hacking our way around the fact that datashift is not a Rails/Spree app/engine
|
77
75
|
# so that we can ** run our specs ** directly in datashift library
|
78
76
|
# i.e without ever having to install datashift in a host application
|
77
|
+
#
|
78
|
+
# NOTES:
|
79
|
+
# => Will chdir into the sandbox to load environment as need to mimic being at root of a rails project
|
80
|
+
# chdir back after environment loaded
|
81
|
+
|
79
82
|
def self.boot( database_env )
|
80
83
|
|
81
84
|
if( ! is_namespace_version )
|
@@ -96,10 +99,18 @@ module DataShift
|
|
96
99
|
|
97
100
|
require 'rails/all'
|
98
101
|
|
102
|
+
store_path = Dir.pwd
|
103
|
+
|
99
104
|
Dir.chdir( File.expand_path('../../../sandbox', __FILE__) )
|
100
105
|
|
106
|
+
rails_root = File.expand_path('../../../sandbox', __FILE__)
|
107
|
+
|
108
|
+
$:.unshift rails_root
|
109
|
+
|
101
110
|
require 'config/environment.rb'
|
102
111
|
|
112
|
+
Dir.chdir( store_path )
|
113
|
+
|
103
114
|
@dslog.info "Booted Spree using post 1.0.0 version"
|
104
115
|
end
|
105
116
|
end
|
data/lib/loaders/csv_loader.rb
CHANGED
@@ -14,8 +14,10 @@ module DataShift
|
|
14
14
|
|
15
15
|
module CsvLoading
|
16
16
|
|
17
|
+
include DataShift::Logging
|
18
|
+
|
17
19
|
def perform_csv_load(file_name, options = {})
|
18
|
-
|
20
|
+
|
19
21
|
require "csv"
|
20
22
|
|
21
23
|
# TODO - can we abstract out what a 'parsed file' is - so a common object can represent excel,csv etc
|
@@ -23,14 +25,12 @@ module DataShift
|
|
23
25
|
|
24
26
|
@parsed_file = CSV.read(file_name)
|
25
27
|
|
26
|
-
|
27
|
-
@method_mapper = DataShift::MethodMapper.new
|
28
|
-
|
29
28
|
@mandatory = options[:mandatory] || []
|
30
29
|
|
31
30
|
# Create a method_mapper which maps list of headers into suitable calls on the Active Record class
|
31
|
+
# For example if model has an attribute 'price' will map columns called Price, price, PRICE etc to this attribute
|
32
32
|
map_headers_to_operators( @parsed_file.shift, options[:strict] , @mandatory )
|
33
|
-
|
33
|
+
|
34
34
|
unless(@method_mapper.missing_methods.empty?)
|
35
35
|
puts "WARNING: Following column headings could not be mapped : #{@method_mapper.missing_methods.inspect}"
|
36
36
|
raise MappingDefinitionError, "ERROR: Missing mappings for #{@method_mapper.missing_methods.size} column headings"
|
@@ -45,6 +45,9 @@ module DataShift
|
|
45
45
|
@loaded_objects = []
|
46
46
|
|
47
47
|
@parsed_file.each do |row|
|
48
|
+
|
49
|
+
# First assign any default values for columns not included in parsed_file
|
50
|
+
process_missing_columns_with_defaults
|
48
51
|
|
49
52
|
# TODO - Smart sorting of column processing order ....
|
50
53
|
# Does not currently ensure mandatory columns (for valid?) processed first but model needs saving
|
@@ -67,7 +70,7 @@ module DataShift
|
|
67
70
|
# TODO - handle when it's not valid ?
|
68
71
|
# Process rest and dump out an exception list of Products ??
|
69
72
|
|
70
|
-
|
73
|
+
logger.info "Saving csv row #{row} to table object : #{load_object.inspect}" #if options[:verbose]
|
71
74
|
|
72
75
|
save
|
73
76
|
|
data/lib/loaders/excel_loader.rb
CHANGED
@@ -80,6 +80,10 @@ module DataShift
|
|
80
80
|
break if @excel.sheet.getRow(row).nil?
|
81
81
|
|
82
82
|
contains_data = false
|
83
|
+
|
84
|
+
# First assign any default values for columns not included in parsed_file
|
85
|
+
process_missing_columns_with_defaults
|
86
|
+
|
83
87
|
|
84
88
|
# TODO - Smart sorting of column processing order ....
|
85
89
|
# Does not currently ensure mandatory columns (for valid?) processed first but model needs saving
|
@@ -87,7 +91,7 @@ module DataShift
|
|
87
91
|
|
88
92
|
# as part of this we also attempt to save early, for example before assigning to
|
89
93
|
# has_and_belongs_to associations which require the load_object has an id for the join table
|
90
|
-
|
94
|
+
|
91
95
|
# Iterate over the columns method_mapper found in Excel,
|
92
96
|
# pulling data out of associated column
|
93
97
|
@method_mapper.method_details.each_with_index do |method_detail, col|
|
data/lib/loaders/loader_base.rb
CHANGED
@@ -16,8 +16,8 @@ module DataShift
|
|
16
16
|
|
17
17
|
class LoaderBase
|
18
18
|
|
19
|
-
|
20
19
|
include DataShift::Logging
|
20
|
+
include DataShift::Populator
|
21
21
|
|
22
22
|
attr_reader :headers
|
23
23
|
|
@@ -131,8 +131,13 @@ module DataShift
|
|
131
131
|
def map_headers_to_operators( headers, strict, mandatory = [])
|
132
132
|
@headers = headers
|
133
133
|
|
134
|
-
|
135
|
-
|
134
|
+
begin
|
135
|
+
method_details = @method_mapper.map_inbound_to_methods( load_object_class, @headers )
|
136
|
+
rescue => e
|
137
|
+
logger.error("Failed to map header row to set of database operators : #{e.inspect}")
|
138
|
+
raise MappingDefinitionError, "Failed to map header row to set of database operators"
|
139
|
+
end
|
140
|
+
|
136
141
|
unless(@method_mapper.missing_methods.empty?)
|
137
142
|
puts "WARNING: Following column headings could not be mapped : #{@method_mapper.missing_methods.inspect}"
|
138
143
|
raise MappingDefinitionError, "Missing mappings for columns : #{@method_mapper.missing_methods.join(",")}" if(strict)
|
@@ -145,6 +150,15 @@ module DataShift
|
|
145
150
|
end
|
146
151
|
|
147
152
|
|
153
|
+
# Process any defaults user has specified, for those columns that are not included in
|
154
|
+
# the incoming import format
|
155
|
+
def process_missing_columns_with_defaults()
|
156
|
+
inbound_ops = @method_mapper.operator_names
|
157
|
+
@default_values.each do |dn, dv|
|
158
|
+
assignment(dn, @load_object, dv) unless(inbound_ops.include?(dn))
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
148
162
|
# Core API - Given a single free text column name from a file, search method mapper for
|
149
163
|
# associated operator on base object class.
|
150
164
|
#
|
@@ -176,25 +190,25 @@ module DataShift
|
|
176
190
|
# IDEAS .....
|
177
191
|
#
|
178
192
|
#unless(@default_data_objects[load_object_class])
|
179
|
-
|
180
|
-
|
193
|
+
#
|
194
|
+
# @default_data_objects[load_object_class] = load_object_class.new
|
181
195
|
|
182
196
|
# default_data_object = @default_data_objects[load_object_class]
|
183
197
|
|
184
198
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
199
|
+
# default_data_object.instance_eval do
|
200
|
+
# def datashift_defaults=(hash)
|
201
|
+
# @datashift_defaults = hash
|
202
|
+
# end
|
203
|
+
# def datashift_defaults
|
204
|
+
# @datashift_defaults
|
205
|
+
# end
|
206
|
+
#end unless load_object_class.respond_to?(:datashift_defaults)
|
193
207
|
#end
|
194
208
|
|
195
209
|
#puts load_object_class.new.to_yaml
|
196
210
|
|
197
|
-
|
211
|
+
logger.info("Read Datashift loading config: #{data.inspect}")
|
198
212
|
|
199
213
|
if(data[load_object_class.name])
|
200
214
|
|