datashift 0.2.2 → 0.4.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 +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
|
|