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.
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