ar_loader 0.0.6 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +9 -9
- data/README.markdown +268 -221
- data/Rakefile +76 -76
- data/lib/VERSION +1 -1
- data/lib/ar_loader.rb +87 -66
- data/lib/ar_loader/exceptions.rb +2 -0
- data/lib/{engine → ar_loader}/file_definitions.rb +353 -353
- data/lib/{engine → ar_loader}/mapping_file_definitions.rb +87 -87
- data/lib/ar_loader/method_detail.rb +257 -0
- data/lib/ar_loader/method_mapper.rb +213 -0
- data/lib/helpers/jruby/jexcel_file.rb +187 -0
- data/lib/{engine → helpers/jruby}/word.rb +79 -70
- data/lib/helpers/spree_helper.rb +85 -0
- data/lib/loaders/csv_loader.rb +87 -0
- data/lib/loaders/excel_loader.rb +132 -0
- data/lib/loaders/loader_base.rb +205 -73
- data/lib/loaders/spree/image_loader.rb +45 -41
- data/lib/loaders/spree/product_loader.rb +140 -91
- data/lib/to_b.rb +24 -24
- data/spec/csv_loader_spec.rb +27 -0
- data/spec/database.yml +19 -6
- data/spec/db/migrate/20110803201325_create_test_bed.rb +78 -0
- data/spec/excel_loader_spec.rb +113 -98
- data/spec/fixtures/BadAssociationName.xls +0 -0
- data/spec/fixtures/DemoNegativeTesting.xls +0 -0
- data/spec/fixtures/DemoTestModelAssoc.xls +0 -0
- data/spec/fixtures/ProjectsMultiCategories.xls +0 -0
- data/spec/fixtures/SimpleProjects.xls +0 -0
- data/spec/fixtures/SpreeProducts.xls +0 -0
- data/spec/fixtures/SpreeZoneExample.csv +5 -0
- data/spec/fixtures/SpreeZoneExample.xls +0 -0
- data/spec/loader_spec.rb +116 -0
- data/spec/logs/test.log +5000 -0
- data/spec/method_mapper_spec.rb +222 -0
- data/spec/models.rb +55 -0
- data/spec/spec_helper.rb +85 -18
- data/spec/spree_loader_spec.rb +223 -157
- data/tasks/config/seed_fu_product_template.erb +15 -15
- data/tasks/config/tidy_config.txt +12 -12
- data/tasks/db_tasks.rake +64 -64
- data/tasks/excel_loader.rake +63 -113
- data/tasks/file_tasks.rake +36 -37
- data/tasks/loader.rake +45 -0
- data/tasks/spree/image_load.rake +108 -107
- data/tasks/spree/product_loader.rake +49 -107
- data/tasks/word_to_seedfu.rake +166 -166
- metadata +66 -61
- data/lib/engine/jruby/jexcel_file.rb +0 -182
- data/lib/engine/jruby/method_mapper_excel.rb +0 -44
- data/lib/engine/method_detail.rb +0 -140
- data/lib/engine/method_mapper.rb +0 -157
- data/lib/engine/method_mapper_csv.rb +0 -28
- data/spec/db/migrate/20110803201325_create_testbed.rb +0 -25
@@ -0,0 +1,132 @@
|
|
1
|
+
# Copyright:: (c) Autotelik Media Ltd 2011
|
2
|
+
# Author :: Tom Statter
|
3
|
+
# Date :: Aug 2011
|
4
|
+
# License:: MIT
|
5
|
+
#
|
6
|
+
# Details:: Specific loader to support Excel files.
|
7
|
+
# Note this only requires JRuby, Excel not required, nor Win OLE
|
8
|
+
#
|
9
|
+
require 'ar_loader/exceptions'
|
10
|
+
|
11
|
+
if(Guards::jruby?)
|
12
|
+
|
13
|
+
|
14
|
+
require 'loaders/loader_base'
|
15
|
+
require 'ar_loader/method_mapper'
|
16
|
+
|
17
|
+
require 'java'
|
18
|
+
require 'jexcel_file'
|
19
|
+
|
20
|
+
module ARLoader
|
21
|
+
|
22
|
+
class ExcelLoader < LoaderBase
|
23
|
+
|
24
|
+
def initialize(klass, object = nil, options = {})
|
25
|
+
super( klass, object, options )
|
26
|
+
raise "Cannot load - failed to create a #{klass}" unless @load_object
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def load( file_name, options = {} )
|
31
|
+
|
32
|
+
@mandatory = options[:mandatory] || nil
|
33
|
+
|
34
|
+
@excel = JExcelFile.new
|
35
|
+
|
36
|
+
@excel.open(file_name)
|
37
|
+
|
38
|
+
sheet_number = options[:sheet_number] || 0
|
39
|
+
|
40
|
+
@sheet = @excel.sheet( sheet_number )
|
41
|
+
|
42
|
+
header_row = options[:header_row] || 0
|
43
|
+
@header_row = @sheet.getRow(header_row)
|
44
|
+
|
45
|
+
raise "ERROR: No headers found - Check Sheet #{@sheet} is completed sheet and Row 1 contains headers" unless(@header_row)
|
46
|
+
|
47
|
+
@headers = []
|
48
|
+
(0..JExcelFile::MAX_COLUMNS).each do |i|
|
49
|
+
cell = @header_row.getCell(i)
|
50
|
+
break unless cell
|
51
|
+
header = "#{@excel.cell_value(cell).to_s}".strip
|
52
|
+
break if header.empty?
|
53
|
+
@headers << header
|
54
|
+
end
|
55
|
+
|
56
|
+
raise "ERROR: No headers found - Check Sheet #{@sheet} is completed sheet and Row 1 contains headers" if(@headers.empty?)
|
57
|
+
|
58
|
+
|
59
|
+
@method_mapper = ARLoader::MethodMapper.new
|
60
|
+
|
61
|
+
# Convert the list of headers into suitable calls on the Active Record class
|
62
|
+
@method_mapper.populate_methods( load_object_class, @headers )
|
63
|
+
|
64
|
+
unless(@method_mapper.missing_methods.empty?)
|
65
|
+
puts "WARNING: Following column headings could not be mapped : #{@method_mapper.missing_methods.inspect}"
|
66
|
+
raise MappingDefinitionError, "ERROR: Missing mappings for #{@method_mapper.missing_methods.size} column headings"
|
67
|
+
end
|
68
|
+
|
69
|
+
unless(@method_mapper.contains_mandatory?( options[:mandatory]) )
|
70
|
+
missing_mandatory( options[:mandatory]).each { |e| puts "ERROR: Mandatory column missing - need a '#{e}' column" }
|
71
|
+
raise "Bad File Description - Mandatory columns missing - please fix and retry."
|
72
|
+
end if(options[:mandatory])
|
73
|
+
|
74
|
+
#if(options[:verbose])
|
75
|
+
puts "\n\n\nLoading from Excel file: #{file_name}"
|
76
|
+
|
77
|
+
load_object_class.transaction do
|
78
|
+
@loaded_objects = []
|
79
|
+
|
80
|
+
(1..@excel.num_rows).collect do |row|
|
81
|
+
|
82
|
+
# Excel num_rows seems to return all 'visible' rows, which appears to be greater than the actual data rows
|
83
|
+
# (TODO - write spec to process .xls with a huge number of rows)
|
84
|
+
#
|
85
|
+
# So currently we have to manually detect when actual data ends, this isn't very smart but
|
86
|
+
# currently got no better idea than ending once we hit the first completely empty row
|
87
|
+
break if @excel.sheet.getRow(row).nil?
|
88
|
+
|
89
|
+
contains_data = false
|
90
|
+
|
91
|
+
# TODO - Smart sorting of column processing order ....
|
92
|
+
# Does not currently ensure mandatory columns (for valid?) processed first but model needs saving
|
93
|
+
# before associations can be processed so user should ensure mandatory columns are prior to associations
|
94
|
+
|
95
|
+
# as part of this we also attempt to save early, for example before assigning to
|
96
|
+
# has_and_belongs_to associations which require the load_object has an id for the join table
|
97
|
+
|
98
|
+
# Iterate over the columns method_mapper found in Excel,
|
99
|
+
# pulling data out of associated column
|
100
|
+
@method_mapper.method_details.each_with_index do |method_detail, col|
|
101
|
+
|
102
|
+
value = value_at(row, col)
|
103
|
+
|
104
|
+
contains_data = true unless(value.nil? || value.to_s.empty?)
|
105
|
+
|
106
|
+
#puts "DEBUG: Excel process METHOD :#{method_detail.inspect}"
|
107
|
+
#puts "DEBUG: Excel process VALUE :#{value.inspect}"
|
108
|
+
process(method_detail, value)
|
109
|
+
end
|
110
|
+
|
111
|
+
break unless contains_data
|
112
|
+
|
113
|
+
# TODO - requirements to handle not valid ?
|
114
|
+
# all or nothing or carry on and dump out the exception list at end
|
115
|
+
|
116
|
+
save
|
117
|
+
|
118
|
+
# don't forget to reset the object or we'll update rather than create
|
119
|
+
new_load_object
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
123
|
+
puts "Excel loading stage complete - #{loaded_objects.size} rows added."
|
124
|
+
end
|
125
|
+
|
126
|
+
def value_at(row, column)
|
127
|
+
@excel.value( @excel.sheet.getRow(row), column)
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
data/lib/loaders/loader_base.rb
CHANGED
@@ -1,74 +1,206 @@
|
|
1
|
-
# Copyright:: (c) Autotelik Media Ltd 2011
|
2
|
-
# Author :: Tom Statter
|
3
|
-
# Date :: Aug 2010
|
4
|
-
# License:: MIT
|
5
|
-
#
|
6
|
-
# Details:: Base class for loaders, providing a process hook which populates a model,
|
7
|
-
# based on a method map and supplied value from a file - i.e a single column/row's string value.
|
8
|
-
# Note that although a single column, the string can be formatted to contain multiple values.
|
9
|
-
#
|
10
|
-
# Tightly coupled with MethodMapper classes (in lib/engine) which contains full details of
|
11
|
-
# a file's column and it's correlated AR associations.
|
12
|
-
#
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
#
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
@
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
1
|
+
# Copyright:: (c) Autotelik Media Ltd 2011
|
2
|
+
# Author :: Tom Statter
|
3
|
+
# Date :: Aug 2010
|
4
|
+
# License:: MIT
|
5
|
+
#
|
6
|
+
# Details:: Base class for loaders, providing a process hook which populates a model,
|
7
|
+
# based on a method map and supplied value from a file - i.e a single column/row's string value.
|
8
|
+
# Note that although a single column, the string can be formatted to contain multiple values.
|
9
|
+
#
|
10
|
+
# Tightly coupled with MethodMapper classes (in lib/engine) which contains full details of
|
11
|
+
# a file's column and it's correlated AR associations.
|
12
|
+
#
|
13
|
+
module ARLoader
|
14
|
+
|
15
|
+
class LoaderBase
|
16
|
+
|
17
|
+
attr_reader :headers
|
18
|
+
|
19
|
+
attr_accessor :load_object_class, :load_object
|
20
|
+
attr_accessor :current_value, :current_method_detail
|
21
|
+
|
22
|
+
attr_accessor :loaded_objects, :failed_objects
|
23
|
+
|
24
|
+
attr_accessor :options
|
25
|
+
|
26
|
+
# Enable an entry representing an association to contain multiple lookup name/value sets in default form :
|
27
|
+
# Name1:value1, value2|Name2:value1, value2, value3|Name3:value1, value2
|
28
|
+
#
|
29
|
+
# E.G.
|
30
|
+
# Row for association could have a columns called Size and once called Colour,
|
31
|
+
# and this combination could be used to lookup multiple associations to add to the main model
|
32
|
+
#
|
33
|
+
# Size:small # => generates find_by_size( 'small' )
|
34
|
+
# Size:large| # => generates find_by_size( 'large' )
|
35
|
+
# Colour:red,green,blue # => generates find_all_by_colour( ['red','green','blue'] )
|
36
|
+
#
|
37
|
+
# TODO - support embedded object creation/update via hash (which hopefully we should be able to just forward to AR)
|
38
|
+
#
|
39
|
+
# |Category|
|
40
|
+
# name:new{ :date => '20110102', :owner = > 'blah'}
|
41
|
+
#
|
42
|
+
@@name_value_delim = ':'
|
43
|
+
@@multi_value_delim = ','
|
44
|
+
|
45
|
+
# TODO - support multi embedded object creation/update via hash (which hopefully we should be able to just forward to AR)
|
46
|
+
#
|
47
|
+
# |Category|
|
48
|
+
# name:new{ :a => 1, :b => 2}|name:medium{ :a => 6, :b => 34}|name:old{ :a => 12, :b => 67}
|
49
|
+
#
|
50
|
+
@@multi_assoc_delim = '|' # Used to delimit multiple entries in a single column
|
51
|
+
|
52
|
+
def self.set_name_value_delim(x) @@name_value_delim = x; end
|
53
|
+
def self.set_multi_value_delim(x) @@multi_value_delim = x; end
|
54
|
+
def self.set_multi_assoc_delim(x) @@multi_assoc_delim = x; end
|
55
|
+
|
56
|
+
# Options
|
57
|
+
def initialize(object_class, object = nil, options = {})
|
58
|
+
@load_object_class = object_class
|
59
|
+
|
60
|
+
# Gather list of all possible 'setter' methods on AR class (instance variables and associations)
|
61
|
+
ARLoader::MethodMapper.find_operators( @load_object_class )
|
62
|
+
|
63
|
+
@options = options.clone
|
64
|
+
@headers = []
|
65
|
+
reset(object)
|
66
|
+
end
|
67
|
+
|
68
|
+
def reset(object = nil)
|
69
|
+
@load_object = object || new_load_object
|
70
|
+
@loaded_objects, @failed_objects = [],[]
|
71
|
+
@current_value = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def contains_mandatory?( mandatory_list )
|
75
|
+
puts "HEADERS", @headers
|
76
|
+
[mandatory_list - @headers].flatten.empty?
|
77
|
+
end
|
78
|
+
|
79
|
+
def missing_mandatory( mandatory_list )
|
80
|
+
[mandatory_list - @headers].flatten
|
81
|
+
end
|
82
|
+
|
83
|
+
def new_load_object
|
84
|
+
@load_object = @load_object_class.new
|
85
|
+
@load_object
|
86
|
+
end
|
87
|
+
|
88
|
+
def abort_on_failure?
|
89
|
+
@options[:abort_on_failure] == 'true'
|
90
|
+
end
|
91
|
+
|
92
|
+
def loaded_count
|
93
|
+
@loaded_objects.size
|
94
|
+
end
|
95
|
+
|
96
|
+
def failed_count
|
97
|
+
@failed_objects.size
|
98
|
+
end
|
99
|
+
|
100
|
+
# Search method mapper for supplied klass and column,
|
101
|
+
# and if suitable association found, process row data into current load_object
|
102
|
+
def find_and_process(klass, column_name, row_data)
|
103
|
+
method_detail = MethodMapper.find_method_detail( klass, column_name )
|
104
|
+
|
105
|
+
|
106
|
+
if(method_detail)
|
107
|
+
process(method_detail, row_data)
|
108
|
+
else
|
109
|
+
@load_object.errors.add_base( "No matching method found for column #{column_name}")
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
# kinda the derived classes interface - best way in Ruby ?
|
115
|
+
def load( input, options = {} )
|
116
|
+
raise "WARNING- ABSTRACT METHOD CALLED - Please implement load()"
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
# What process a value string from a column.
|
121
|
+
# Assigning value(s) to correct association on @load_object.
|
122
|
+
# Method detail represents a column from a file and it's correlated AR associations.
|
123
|
+
# Value string which may contain multiple values for a collection association.
|
124
|
+
#
|
125
|
+
def process(method_detail, value)
|
126
|
+
#puts "INFO: LOADER BASE processing #{@load_object}"
|
127
|
+
@current_value = value
|
128
|
+
@current_method_detail = method_detail
|
129
|
+
|
130
|
+
if(method_detail.operator_for(:has_many))
|
131
|
+
|
132
|
+
if(method_detail.operator_class && @current_value)
|
133
|
+
|
134
|
+
# there are times when we need to save early, for example before assigning to
|
135
|
+
# has_and_belongs_to associations which require the load_object has an id for the join table
|
136
|
+
|
137
|
+
save if( load_object.valid? && load_object.new_record? )
|
138
|
+
|
139
|
+
# A single column can contain multiple associations delimited by special char
|
140
|
+
columns = @current_value.split(@@multi_assoc_delim)
|
141
|
+
|
142
|
+
# Size:large|Colour:red,green,blue => generates find_by_size( 'large' ) and find_all_by_colour( ['red','green','blue'] )
|
143
|
+
|
144
|
+
columns.each do |assoc|
|
145
|
+
operator, values = assoc.split(@@name_value_delim)
|
146
|
+
|
147
|
+
lookups = values.split(@@multi_value_delim)
|
148
|
+
|
149
|
+
if(lookups.size > 1)
|
150
|
+
|
151
|
+
@current_value = method_detail.operator_class.send("find_all_by_#{operator}", lookups )
|
152
|
+
|
153
|
+
unless(lookups.size == @current_value.size)
|
154
|
+
found = @current_value.collect {|f| f.send(operator) }
|
155
|
+
@load_object.errors.add( method_detail.operator, "Association with key(s) #{(lookups - found).inspect} NOT found")
|
156
|
+
puts "WARNING: Association with key(s) #{(lookups - found).inspect} NOT found - Not added."
|
157
|
+
next if(@current_value.empty?)
|
158
|
+
end
|
159
|
+
|
160
|
+
else
|
161
|
+
|
162
|
+
@current_value = method_detail.operator_class.send("find_by_#{operator}", lookups )
|
163
|
+
|
164
|
+
unless(@current_value)
|
165
|
+
@load_object.errors.add( method_detail.operator, "Association with key #{lookups} NOT found")
|
166
|
+
puts "WARNING: Association with key #{lookups} NOT found - Not added."
|
167
|
+
next
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
# Lookup Assoc's Model done, now add the found value(s) to load model's collection
|
173
|
+
method_detail.assign(@load_object, @current_value)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
# END HAS_MANY
|
177
|
+
else
|
178
|
+
# Nice n simple straight assignment to a column variable
|
179
|
+
method_detail.assign(@load_object, @current_value)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def save
|
184
|
+
puts "SAVING #{load_object.class} : #{load_object.inspect}" #if(options[:verbose])
|
185
|
+
begin
|
186
|
+
@load_object.save
|
187
|
+
@loaded_objects << load_object unless(@loaded_objects.include?(load_object))
|
188
|
+
rescue => e
|
189
|
+
@failed_objects << load_object unless(@failed_objects.include?(load_object))
|
190
|
+
puts "Error saving #{load_object.class} : #{e.inspect}"
|
191
|
+
raise "Error in save whilst processing column #{@current_method_detail.name}" if(@options[:strict])
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def find_or_new( klass, condition_hash = {} )
|
196
|
+
@records[klass] = klass.find(:all, :conditions => condition_hash)
|
197
|
+
if @records[klass].any?
|
198
|
+
return @records[klass].first
|
199
|
+
else
|
200
|
+
return klass.new
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
|
74
206
|
end
|
@@ -1,42 +1,46 @@
|
|
1
|
-
# Copyright:: (c) Autotelik Media Ltd 2011
|
2
|
-
# Author :: Tom Statter
|
3
|
-
# Date :: Jan 2011
|
4
|
-
# License:: MIT. Free, Open Source.
|
5
|
-
#
|
6
|
-
require 'loader_base'
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
1
|
+
# Copyright:: (c) Autotelik Media Ltd 2011
|
2
|
+
# Author :: Tom Statter
|
3
|
+
# Date :: Jan 2011
|
4
|
+
# License:: MIT. Free, Open Source.
|
5
|
+
#
|
6
|
+
require 'loader_base'
|
7
|
+
|
8
|
+
module ARLoader
|
9
|
+
|
10
|
+
class ImageLoader < LoaderBase
|
11
|
+
|
12
|
+
def initialize(image = nil)
|
13
|
+
super( Image, image )
|
14
|
+
raise "Failed to create Image for loading" unless @load_object
|
15
|
+
end
|
16
|
+
|
17
|
+
# Note the Spree Image model sets default storage path to
|
18
|
+
# => :path => ":rails_root/public/assets/products/:id/:style/:basename.:extension"
|
19
|
+
|
20
|
+
def process( image_path, record = nil)
|
21
|
+
|
22
|
+
unless File.exists?(image_path)
|
23
|
+
puts "ERROR : Invalid Path"
|
24
|
+
return
|
25
|
+
end
|
26
|
+
|
27
|
+
alt = (record and record.respond_to? :name) ? record.name : ""
|
28
|
+
|
29
|
+
@load_object.alt = alt
|
30
|
+
|
31
|
+
begin
|
32
|
+
@load_object.attachment = File.new(image_path, "r")
|
33
|
+
rescue => e
|
34
|
+
puts e.inspect
|
35
|
+
puts "ERROR : Failed to read image #{image_path}"
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
@load_object.attachment.reprocess!
|
40
|
+
@load_object.viewable = record if record
|
41
|
+
|
42
|
+
puts @load_object.save ? "Success: Uploaded Image: #{@load_object.inspect}" : "ERROR : Problem saving to DB Image: #{@load_object}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
42
46
|
end
|