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.
Files changed (53) hide show
  1. data/LICENSE +9 -9
  2. data/README.markdown +268 -221
  3. data/Rakefile +76 -76
  4. data/lib/VERSION +1 -1
  5. data/lib/ar_loader.rb +87 -66
  6. data/lib/ar_loader/exceptions.rb +2 -0
  7. data/lib/{engine → ar_loader}/file_definitions.rb +353 -353
  8. data/lib/{engine → ar_loader}/mapping_file_definitions.rb +87 -87
  9. data/lib/ar_loader/method_detail.rb +257 -0
  10. data/lib/ar_loader/method_mapper.rb +213 -0
  11. data/lib/helpers/jruby/jexcel_file.rb +187 -0
  12. data/lib/{engine → helpers/jruby}/word.rb +79 -70
  13. data/lib/helpers/spree_helper.rb +85 -0
  14. data/lib/loaders/csv_loader.rb +87 -0
  15. data/lib/loaders/excel_loader.rb +132 -0
  16. data/lib/loaders/loader_base.rb +205 -73
  17. data/lib/loaders/spree/image_loader.rb +45 -41
  18. data/lib/loaders/spree/product_loader.rb +140 -91
  19. data/lib/to_b.rb +24 -24
  20. data/spec/csv_loader_spec.rb +27 -0
  21. data/spec/database.yml +19 -6
  22. data/spec/db/migrate/20110803201325_create_test_bed.rb +78 -0
  23. data/spec/excel_loader_spec.rb +113 -98
  24. data/spec/fixtures/BadAssociationName.xls +0 -0
  25. data/spec/fixtures/DemoNegativeTesting.xls +0 -0
  26. data/spec/fixtures/DemoTestModelAssoc.xls +0 -0
  27. data/spec/fixtures/ProjectsMultiCategories.xls +0 -0
  28. data/spec/fixtures/SimpleProjects.xls +0 -0
  29. data/spec/fixtures/SpreeProducts.xls +0 -0
  30. data/spec/fixtures/SpreeZoneExample.csv +5 -0
  31. data/spec/fixtures/SpreeZoneExample.xls +0 -0
  32. data/spec/loader_spec.rb +116 -0
  33. data/spec/logs/test.log +5000 -0
  34. data/spec/method_mapper_spec.rb +222 -0
  35. data/spec/models.rb +55 -0
  36. data/spec/spec_helper.rb +85 -18
  37. data/spec/spree_loader_spec.rb +223 -157
  38. data/tasks/config/seed_fu_product_template.erb +15 -15
  39. data/tasks/config/tidy_config.txt +12 -12
  40. data/tasks/db_tasks.rake +64 -64
  41. data/tasks/excel_loader.rake +63 -113
  42. data/tasks/file_tasks.rake +36 -37
  43. data/tasks/loader.rake +45 -0
  44. data/tasks/spree/image_load.rake +108 -107
  45. data/tasks/spree/product_loader.rake +49 -107
  46. data/tasks/word_to_seedfu.rake +166 -166
  47. metadata +66 -61
  48. data/lib/engine/jruby/jexcel_file.rb +0 -182
  49. data/lib/engine/jruby/method_mapper_excel.rb +0 -44
  50. data/lib/engine/method_detail.rb +0 -140
  51. data/lib/engine/method_mapper.rb +0 -157
  52. data/lib/engine/method_mapper_csv.rb +0 -28
  53. 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
@@ -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
- class LoaderBase
14
-
15
- attr_accessor :load_object_class, :load_object, :value
16
-
17
- # Enable single column (association) to contain multiple name/value sets in default form :
18
- # Name1:value1, value2|Name2:value1, value2, value3|Name3:value1, value2
19
- #
20
- # E.G.
21
- # Row for association could have a name (Size/Colour/Sex) with a set of values,
22
- # and this combination can be expressed multiple times :
23
- # Size:small,medium,large|Colour:red, green|Sex:Female
24
-
25
- @@name_value_delim = ':'
26
- @@multi_value_delim = ','
27
- @@multi_assoc_delim = '|'
28
-
29
- def self.set_name_value_delim(x) @@name_value_delim = x; end
30
- def self.set_multi_value_delim(x) @@multi_value_delim = x; end
31
- def self.set_multi_assoc_delim(x) @@multi_assoc_delim = x; end
32
-
33
- def initialize(object_class, object = nil)
34
- @load_object_class = object_class
35
- @load_object = object || @load_object_class.new
36
- end
37
-
38
- def reset()
39
- @load_object = @load_object_class.new
40
- end
41
-
42
- # What process a value string from a column.
43
- # Assigning value(s) to correct association on @load_object.
44
- # Method map represents a column from a file and it's correlated AR associations.
45
- # Value string which may contain multiple values for a collection association.
46
- #
47
- def process(method_map, value)
48
- #puts "INFO: LOADER BASE processing #{@load_object}"
49
- @value = value
50
-
51
- if(method_map.has_many && method_map.has_many_class && @value)
52
- # The Generic handler for Associations
53
- # The actual class of the association so we can find_or_create on it
54
- assoc_class = method_map.has_many_class
55
-
56
- puts "Processing Association: #{assoc_class} : #{@value}"
57
-
58
- @value.split(@@multi_assoc_delim).collect do |lookup|
59
- # TODO - Don't just rely on 'name' but try different finds as per MethodMappe::insistent_belongs_to ..
60
- x = assoc_class.find(:first, :conditions => ['lower(name) LIKE ?', "%#{lookup.downcase}%"])
61
- unless x
62
- puts "WARNING: #{lookup} in #{assoc_class} NOT found - Not added to #{@load_object.class}"
63
- next
64
- end
65
- @load_object.send( method_map.has_many ) << x
66
- @load_object.save
67
- end
68
- else
69
- # Nice n simple straight assignment to a column variable
70
- method_map.assign(@load_object, @value) unless method_map.has_many
71
- end
72
- end
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
- class ImageLoader < LoaderBase
9
-
10
- def initialize(klass = Image, image = nil)
11
- super( klass, image )
12
- raise "Failed to create Image for loading" unless @load_object
13
- end
14
-
15
- # Note the Spree Image model sets default storage path to
16
- # => :path => ":rails_root/public/assets/products/:id/:style/:basename.:extension"
17
-
18
- def process( image_path, record = nil)
19
-
20
- unless File.exists?(image_path)
21
- puts "ERROR : Invalid Path"
22
- return
23
- end
24
-
25
- alt = (record and record.respond_to? :name) ? record.name : ""
26
-
27
- @load_object.alt = alt
28
-
29
- begin
30
- @load_object.attachment = File.new(image_path, "r")
31
- rescue => e
32
- puts e.inspect
33
- puts "ERROR : Failed to read image #{image_path}"
34
- return
35
- end
36
-
37
- @load_object.attachment.reprocess!
38
- @load_object.viewable = record if record
39
-
40
- puts @load_object.save ? "Success: Uploaded Image: #{@load_object.inspect}" : "ERROR : Problem saving to DB Image: #{@load_object}"
41
- end
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