ar_loader 0.0.6 → 0.0.8
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/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
|