datashift 0.2.1 → 0.2.2

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 (84) hide show
  1. data/.document +5 -5
  2. data/LICENSE.txt +26 -26
  3. data/README.markdown +326 -305
  4. data/README.rdoc +19 -19
  5. data/Rakefile +86 -93
  6. data/VERSION +1 -1
  7. data/datashift.gemspec +163 -152
  8. data/lib/applications/jruby/jexcel_file.rb +410 -408
  9. data/lib/applications/jruby/word.rb +79 -79
  10. data/lib/datashift.rb +183 -152
  11. data/lib/datashift/exceptions.rb +11 -11
  12. data/lib/datashift/file_definitions.rb +353 -353
  13. data/lib/datashift/mapping_file_definitions.rb +87 -87
  14. data/lib/datashift/method_detail.rb +293 -275
  15. data/lib/datashift/method_dictionary.rb +208 -209
  16. data/lib/datashift/method_mapper.rb +90 -90
  17. data/lib/datashift/model_mapper.rb +27 -0
  18. data/lib/exporters/csv_exporter.rb +36 -0
  19. data/lib/exporters/excel_exporter.rb +116 -0
  20. data/lib/exporters/exporter_base.rb +15 -0
  21. data/lib/generators/csv_generator.rb +36 -36
  22. data/lib/generators/excel_generator.rb +106 -122
  23. data/lib/generators/generator_base.rb +13 -13
  24. data/lib/helpers/core_ext/to_b.rb +24 -24
  25. data/lib/helpers/rake_utils.rb +42 -0
  26. data/lib/helpers/spree_helper.rb +194 -153
  27. data/lib/java/poi-3.7/LICENSE +507 -507
  28. data/lib/java/poi-3.7/NOTICE +21 -21
  29. data/lib/java/poi-3.7/RELEASE_NOTES.txt +115 -115
  30. data/lib/loaders/csv_loader.rb +98 -98
  31. data/lib/loaders/excel_loader.rb +155 -155
  32. data/lib/loaders/loader_base.rb +420 -420
  33. data/lib/loaders/spreadsheet_loader.rb +136 -136
  34. data/lib/loaders/spree/image_loader.rb +67 -63
  35. data/lib/loaders/spree/product_loader.rb +289 -248
  36. data/lib/thor/generate_excel.thor +54 -0
  37. data/sandbox/app/controllers/application_controller.rb +3 -0
  38. data/sandbox/config/application.rb +43 -0
  39. data/sandbox/config/database.yml +34 -0
  40. data/sandbox/config/environment.rb +7 -0
  41. data/sandbox/config/environments/development.rb +30 -0
  42. data/spec/csv_loader_spec.rb +30 -30
  43. data/spec/datashift_spec.rb +26 -26
  44. data/spec/db/migrate/20110803201325_create_test_bed.rb +85 -85
  45. data/spec/excel_exporter_spec.rb +78 -78
  46. data/spec/excel_generator_spec.rb +78 -78
  47. data/spec/excel_loader_spec.rb +223 -223
  48. data/spec/file_definitions.rb +141 -141
  49. data/spec/fixtures/ProjectsDefaults.yml +29 -29
  50. data/spec/fixtures/config/database.yml +27 -27
  51. data/spec/fixtures/datashift_Spree_db.sqlite +0 -0
  52. data/spec/fixtures/datashift_test_models_db.sqlite +0 -0
  53. data/spec/fixtures/negative/SpreeProdMiss1Mandatory.csv +4 -4
  54. data/spec/fixtures/negative/SpreeProdMissManyMandatory.csv +4 -4
  55. data/spec/fixtures/spree/SpreeProducts.csv +4 -4
  56. data/spec/fixtures/spree/SpreeProducts.xls +0 -0
  57. data/spec/fixtures/spree/SpreeProductsMultiColumn.csv +4 -4
  58. data/spec/fixtures/spree/SpreeProductsMultiColumn.xls +0 -0
  59. data/spec/fixtures/spree/SpreeProductsSimple.csv +4 -4
  60. data/spec/fixtures/spree/SpreeProductsWithImages.csv +4 -4
  61. data/spec/fixtures/spree/SpreeZoneExample.csv +5 -5
  62. data/spec/fixtures/test_model_defs.rb +57 -57
  63. data/spec/loader_spec.rb +120 -120
  64. data/spec/method_dictionary_spec.rb +242 -242
  65. data/spec/method_mapper_spec.rb +41 -41
  66. data/spec/spec_helper.rb +154 -116
  67. data/spec/spree_exporter_spec.rb +67 -0
  68. data/spec/spree_generator_spec.rb +77 -64
  69. data/spec/spree_loader_spec.rb +363 -324
  70. data/spec/spree_method_mapping_spec.rb +218 -214
  71. data/tasks/config/seed_fu_product_template.erb +15 -15
  72. data/tasks/config/tidy_config.txt +12 -12
  73. data/tasks/{excel_generator.rake → export/excel_generator.rake} +101 -78
  74. data/tasks/file_tasks.rake +36 -36
  75. data/tasks/import/csv.rake +50 -49
  76. data/tasks/import/excel.rake +74 -71
  77. data/tasks/spree/image_load.rake +108 -108
  78. data/tasks/spree/product_loader.rake +43 -43
  79. data/tasks/word_to_seedfu.rake +166 -166
  80. data/test/helper.rb +18 -18
  81. data/test/test_interact.rb +7 -7
  82. metadata +16 -8
  83. data/datashift-0.1.0.gem +0 -0
  84. data/tasks/db_tasks.rake +0 -66
@@ -1,137 +1,137 @@
1
- # Copyright:: (c) Autotelik Media Ltd 2011
2
- # Author :: Tom Statter
3
- # Date :: Jan 2012
4
- # License:: MIT
5
- #
6
- # Details:: Specific loader to support Excel files via http://rubygems.org/gems/spreadsheet
7
- #
8
- # Offers an alternative for non JRuby usage(see excel_loader)
9
- #
10
- # Maps column headings to operations on the model.
11
- # Iterates over all the rows using mapped operations to assign row data to a database object,
12
- # i.e pulls data from each column and sends to object.
13
- #
14
- require 'datashift/exceptions'
15
-
16
- module DataShift
17
-
18
- require 'loaders/loader_base'
19
-
20
- module SpreadsheetLoading
21
-
22
- # Options:
23
- # [:header_row] : Default is 0. Use alternative row as header definition.
24
- # [:mandatory] : Array of mandatory column names
25
- # [:strict] : Raise exception when no mapping found for a column heading (non mandatory)
26
- # [:sheet_number]
27
-
28
- def perform_spreadsheet_load( file_name, options = {} )
29
-
30
- @mandatory = options[:mandatory] || []
31
-
32
- @excel = JExcelFile.new
33
-
34
- @excel.open(file_name)
35
-
36
- #if(options[:verbose])
37
- puts "\n\n\nLoading from Excel file: #{file_name}"
38
-
39
- sheet_number = options[:sheet_number] || 0
40
-
41
- @sheet = @excel.sheet( sheet_number )
42
-
43
- header_row_index = options[:header_row] || 0
44
- @header_row = @sheet.getRow(header_row_index)
45
-
46
- raise MissingHeadersError, "No headers found - Check Sheet #{@sheet} is complete and Row #{header_row_index} contains headers" unless(@header_row)
47
-
48
- @headers = []
49
-
50
- (0..JExcelFile::MAX_COLUMNS).each do |i|
51
- cell = @header_row.getCell(i)
52
- break unless cell
53
- header = "#{@excel.cell_value(cell).to_s}".strip
54
- break if header.empty?
55
- @headers << header
56
- end
57
-
58
- raise MissingHeadersError, "No headers found - Check Sheet #{@sheet} is complete and Row #{header_row_index} contains headers" if(@headers.empty?)
59
-
60
- # Create a method_mapper which maps list of headers into suitable calls on the Active Record class
61
- map_headers_to_operators( @headers, options[:strict] , @mandatory )
62
-
63
- load_object_class.transaction do
64
- @loaded_objects = []
65
-
66
- (1..@excel.num_rows).collect do |row|
67
-
68
- # Excel num_rows seems to return all 'visible' rows, which appears to be greater than the actual data rows
69
- # (TODO - write spec to process .xls with a huge number of rows)
70
- #
71
- # This is rubbish but currently manually detect when actual data ends, this isn't very smart but
72
- # got no better idea than ending once we hit the first completely empty row
73
- break if @excel.sheet.getRow(row).nil?
74
-
75
- contains_data = false
76
-
77
- # TODO - Smart sorting of column processing order ....
78
- # Does not currently ensure mandatory columns (for valid?) processed first but model needs saving
79
- # before associations can be processed so user should ensure mandatory columns are prior to associations
80
-
81
- # as part of this we also attempt to save early, for example before assigning to
82
- # has_and_belongs_to associations which require the load_object has an id for the join table
83
-
84
- # Iterate over the columns method_mapper found in Excel,
85
- # pulling data out of associated column
86
- @method_mapper.method_details.each_with_index do |method_detail, col|
87
-
88
- value = value_at(row, col)
89
-
90
- contains_data = true unless(value.nil? || value.to_s.empty?)
91
-
92
- #puts "DEBUG: Excel process METHOD :#{method_detail.inspect}", value.inspect
93
- prepare_data(method_detail, value)
94
-
95
- process()
96
- end
97
-
98
- break unless(contains_data == true)
99
-
100
- # TODO - requirements to handle not valid ?
101
- # all or nothing or carry on and dump out the exception list at end
102
- #puts "DEBUG: FINAL SAVE #{load_object.inspect}"
103
- save
104
- #puts "DEBUG: SAVED #{load_object.inspect}"
105
-
106
- # don't forget to reset the object or we'll update rather than create
107
- new_load_object
108
-
109
- end
110
- end
111
- puts "Spreadsheet loading stage complete - #{loaded_objects.size} rows added."
112
- end
113
-
114
- def value_at(row, column)
115
- @excel.get_cell_value( @excel.sheet.getRow(row), column)
116
- end
117
- end
118
-
119
-
120
- class SpreadsheetLoader < LoaderBase
121
-
122
- include SpreadsheetLoading
123
-
124
- def initialize(klass, object = nil, options = {})
125
- super( klass, object, options )
126
- raise "Cannot load - failed to create a #{klass}" unless @load_object
127
- end
128
-
129
- def perform_load( file_name, options = {} )
130
- perform_spreadsheet_load( file_name, options )
131
-
132
- puts "Spreadsheet loading stage complete - #{loaded_objects.size} rows added."
133
- end
134
-
135
- end
136
-
1
+ # Copyright:: (c) Autotelik Media Ltd 2011
2
+ # Author :: Tom Statter
3
+ # Date :: Jan 2012
4
+ # License:: MIT
5
+ #
6
+ # Details:: Specific loader to support Excel files via http://rubygems.org/gems/spreadsheet
7
+ #
8
+ # Offers an alternative for non JRuby usage(see excel_loader)
9
+ #
10
+ # Maps column headings to operations on the model.
11
+ # Iterates over all the rows using mapped operations to assign row data to a database object,
12
+ # i.e pulls data from each column and sends to object.
13
+ #
14
+ require 'datashift/exceptions'
15
+
16
+ module DataShift
17
+
18
+ require 'loaders/loader_base'
19
+
20
+ module SpreadsheetLoading
21
+
22
+ # Options:
23
+ # [:header_row] : Default is 0. Use alternative row as header definition.
24
+ # [:mandatory] : Array of mandatory column names
25
+ # [:strict] : Raise exception when no mapping found for a column heading (non mandatory)
26
+ # [:sheet_number]
27
+
28
+ def perform_spreadsheet_load( file_name, options = {} )
29
+
30
+ @mandatory = options[:mandatory] || []
31
+
32
+ @excel = JExcelFile.new
33
+
34
+ @excel.open(file_name)
35
+
36
+ #if(options[:verbose])
37
+ puts "\n\n\nLoading from Excel file: #{file_name}"
38
+
39
+ sheet_number = options[:sheet_number] || 0
40
+
41
+ @sheet = @excel.sheet( sheet_number )
42
+
43
+ header_row_index = options[:header_row] || 0
44
+ @header_row = @sheet.getRow(header_row_index)
45
+
46
+ raise MissingHeadersError, "No headers found - Check Sheet #{@sheet} is complete and Row #{header_row_index} contains headers" unless(@header_row)
47
+
48
+ @headers = []
49
+
50
+ (0..JExcelFile::MAX_COLUMNS).each do |i|
51
+ cell = @header_row.getCell(i)
52
+ break unless cell
53
+ header = "#{@excel.cell_value(cell).to_s}".strip
54
+ break if header.empty?
55
+ @headers << header
56
+ end
57
+
58
+ raise MissingHeadersError, "No headers found - Check Sheet #{@sheet} is complete and Row #{header_row_index} contains headers" if(@headers.empty?)
59
+
60
+ # Create a method_mapper which maps list of headers into suitable calls on the Active Record class
61
+ map_headers_to_operators( @headers, options[:strict] , @mandatory )
62
+
63
+ load_object_class.transaction do
64
+ @loaded_objects = []
65
+
66
+ (1..@excel.num_rows).collect do |row|
67
+
68
+ # Excel num_rows seems to return all 'visible' rows, which appears to be greater than the actual data rows
69
+ # (TODO - write spec to process .xls with a huge number of rows)
70
+ #
71
+ # This is rubbish but currently manually detect when actual data ends, this isn't very smart but
72
+ # got no better idea than ending once we hit the first completely empty row
73
+ break if @excel.sheet.getRow(row).nil?
74
+
75
+ contains_data = false
76
+
77
+ # TODO - Smart sorting of column processing order ....
78
+ # Does not currently ensure mandatory columns (for valid?) processed first but model needs saving
79
+ # before associations can be processed so user should ensure mandatory columns are prior to associations
80
+
81
+ # as part of this we also attempt to save early, for example before assigning to
82
+ # has_and_belongs_to associations which require the load_object has an id for the join table
83
+
84
+ # Iterate over the columns method_mapper found in Excel,
85
+ # pulling data out of associated column
86
+ @method_mapper.method_details.each_with_index do |method_detail, col|
87
+
88
+ value = value_at(row, col)
89
+
90
+ contains_data = true unless(value.nil? || value.to_s.empty?)
91
+
92
+ #puts "DEBUG: Excel process METHOD :#{method_detail.inspect}", value.inspect
93
+ prepare_data(method_detail, value)
94
+
95
+ process()
96
+ end
97
+
98
+ break unless(contains_data == true)
99
+
100
+ # TODO - requirements to handle not valid ?
101
+ # all or nothing or carry on and dump out the exception list at end
102
+ #puts "DEBUG: FINAL SAVE #{load_object.inspect}"
103
+ save
104
+ #puts "DEBUG: SAVED #{load_object.inspect}"
105
+
106
+ # don't forget to reset the object or we'll update rather than create
107
+ new_load_object
108
+
109
+ end
110
+ end
111
+ puts "Spreadsheet loading stage complete - #{loaded_objects.size} rows added."
112
+ end
113
+
114
+ def value_at(row, column)
115
+ @excel.get_cell_value( @excel.sheet.getRow(row), column)
116
+ end
117
+ end
118
+
119
+
120
+ class SpreadsheetLoader < LoaderBase
121
+
122
+ include SpreadsheetLoading
123
+
124
+ def initialize(klass, object = nil, options = {})
125
+ super( klass, object, options )
126
+ raise "Cannot load - failed to create a #{klass}" unless @load_object
127
+ end
128
+
129
+ def perform_load( file_name, options = {} )
130
+ perform_spreadsheet_load( file_name, options )
131
+
132
+ puts "Spreadsheet loading stage complete - #{loaded_objects.size} rows added."
133
+ end
134
+
135
+ end
136
+
137
137
  end
@@ -1,64 +1,68 @@
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 DataShift
9
-
10
- module ImageLoading
11
-
12
- # Note the Spree Image model sets default storage path to
13
- # => :path => ":rails_root/public/assets/products/:id/:style/:basename.:extension"
14
-
15
- def create_image(image_path, viewable_record = nil, options = {})
16
-
17
- image = Image.new
18
-
19
- unless File.exists?(image_path)
20
- puts "ERROR : Invalid Path"
21
- return image
22
- end
23
-
24
- alt = if(options[:alt])
25
- options[:alt]
26
- else
27
- (viewable_record and viewable_record.respond_to? :name) ? viewable_record.name : ""
28
- end
29
-
30
- image.alt = alt
31
-
32
- begin
33
- image.attachment = File.new(image_path, "r")
34
- rescue => e
35
- puts e.inspect
36
- puts "ERROR : Failed to read image #{image_path}"
37
- return image
38
- end
39
-
40
- image.attachment.reprocess!
41
- image.viewable = viewable_record if viewable_record
42
-
43
- puts image.save ? "Success: Created Image: #{image.inspect}" : "ERROR : Problem saving to DB Image: #{image.inspect}"
44
- end
45
- end
46
-
47
- class ImageLoader < LoaderBase
48
-
49
- include DataShift::ImageLoading
50
-
51
- def initialize(image = nil)
52
- super( Image, image )
53
- raise "Failed to create Image for loading" unless @load_object
54
- end
55
-
56
- # Note the Spree Image model sets default storage path to
57
- # => :path => ":rails_root/public/assets/products/:id/:style/:basename.:extension"
58
-
59
- def process( image_path, record = nil)
60
- @load_object = create_image(path, record)
61
- end
62
- end
63
-
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 DataShift
9
+
10
+ module ImageLoading
11
+
12
+ # Note the Spree Image model sets default storage path to
13
+ # => :path => ":rails_root/public/assets/products/:id/:style/:basename.:extension"
14
+
15
+ def create_image(image_path, viewable_record = nil, options = {})
16
+
17
+ @@image_klass ||= SpreeHelper::get_spree_class('Image')
18
+
19
+ image = @@image_klass.new
20
+
21
+ unless File.exists?(image_path)
22
+ puts "ERROR : Invalid Path"
23
+ return image
24
+ end
25
+
26
+ alt = if(options[:alt])
27
+ options[:alt]
28
+ else
29
+ (viewable_record and viewable_record.respond_to? :name) ? viewable_record.name : ""
30
+ end
31
+
32
+ image.alt = alt
33
+
34
+ begin
35
+ image.attachment = File.new(image_path, "r")
36
+ rescue => e
37
+ puts e.inspect
38
+ puts "ERROR : Failed to read image #{image_path}"
39
+ return image
40
+ end
41
+
42
+ image.attachment.reprocess!
43
+ image.viewable = viewable_record if viewable_record
44
+
45
+ puts image.save ? "Success: Created Image: #{image.inspect}" : "ERROR : Problem saving to DB Image: #{image.inspect}"
46
+ end
47
+ end
48
+
49
+ class ImageLoader < LoaderBase
50
+
51
+ include DataShift::ImageLoading
52
+
53
+ def initialize(image = nil)
54
+ @@image_klass ||= SpreeHelper::get_spree_class('Image')
55
+
56
+ super( @@image_klass, image )
57
+ raise "Failed to create Image for loading" unless @load_object
58
+ end
59
+
60
+ # Note the Spree Image model sets default storage path to
61
+ # => :path => ":rails_root/public/assets/products/:id/:style/:basename.:extension"
62
+
63
+ def process( image_path, record = nil)
64
+ @load_object = create_image(path, record)
65
+ end
66
+ end
67
+
64
68
  end
@@ -1,249 +1,290 @@
1
- # Copyright:: (c) Autotelik Media Ltd 2010
2
- # Author :: Tom Statter
3
- # Date :: Aug 2010
4
- # License:: MIT ?
5
- #
6
- # Details:: Specific over-rides/additions to support Spree Products
7
- #
8
- require 'loader_base'
9
- require 'csv_loader'
10
- require 'excel_loader'
11
- require 'image_loader'
12
-
13
- module DataShift
14
-
15
- module Spree
16
-
17
- class ProductLoader < LoaderBase
18
-
19
- include DataShift::CsvLoading
20
- include DataShift::ExcelLoading
21
- include DataShift::ImageLoading
22
-
23
- def initialize(product = nil)
24
- super( Product, product, :instance_methods => true )
25
- raise "Failed to create Product for loading" unless @load_object
26
- end
27
-
28
- # Based on filename call appropriate loading function
29
- # Currently supports :
30
- # Excel/Open Office files saved as .xls
31
- # CSV files
32
- def perform_load( file_name, options = {} )
33
-
34
- ext = File.extname(file_name)
35
-
36
- if(ext == '.xls')
37
- raise DataShift::BadRuby, "Please install and use JRuby for loading .xls files" unless(Guards::jruby?)
38
- perform_excel_load(file_name, options)
39
- elsif(ext == '.csv')
40
- perform_csv_load(file_name, options)
41
- else
42
- raise DataShift::UnsupportedFileType, "#{ext} files not supported - Try CSV or OpenOffice/Excel .xls"
43
- end
44
- end
45
-
46
- # Over ride base class process with some Spree::Product specifics
47
- #
48
- # What process a value string from a column, assigning value(s) to correct association on Product.
49
- # Method map represents a column from a file and it's correlated Product association.
50
- # Value string which may contain multiple values for a collection (has_many) association.
51
- #
52
- def process()
53
-
54
- # Special cases for Products, generally where a simple one stage lookup won't suffice
55
- # otherwise simply use default processing from base class
56
- if((@current_method_detail.operator?('variants') || @current_method_detail.operator?('option_types')) && current_value)
57
-
58
- add_options
59
-
60
- elsif(@current_method_detail.operator?('taxons') && current_value)
61
-
62
- add_taxons
63
-
64
- elsif(@current_method_detail.operator?('product_properties') && current_value)
65
-
66
- add_properties
67
-
68
- elsif(@current_method_detail.operator?('images') && current_value)
69
-
70
- add_images
71
-
72
- elsif(@current_method_detail.operator?('count_on_hand') || @current_method_detail.operator?('on_hand') )
73
-
74
- # Unless we can save here, in danger of count_on_hand getting wiped out.
75
- # If we set (on_hand or count_on_hand) on an unsaved object, during next subsequent save
76
- # looks like some validation code or something calls Variant.on_hand= with 0
77
- # If we save first, then our values seem to stick
78
-
79
- # TODO smart column ordering to ensure always valid - if we always make it very last column might not get wiped ?
80
- #
81
- save_if_new
82
-
83
- # Spree has some stock management stuff going on, so dont usually assign to column vut use
84
- # on_hand and on_hand=
85
- if(@load_object.variants.size > 0 && current_value.include?(LoaderBase::multi_assoc_delim))
86
-
87
- #puts "DEBUG: COUNT_ON_HAND PER VARIANT",current_value.is_a?(String),
88
- #&& current_value.is_a?(String) && current_value.include?(LoaderBase::multi_assoc_delim))
89
- # Check if we processed Option Types and assign count per option
90
- values = current_value.to_s.split(LoaderBase::multi_assoc_delim)
91
-
92
- if(@load_object.variants.size == values.size)
93
- @load_object.variants.each_with_index {|v, i| v.on_hand = values[i]; v.save; }
94
- else
95
- puts "WARNING: Count on hand entries did not match number of Variants - None Set"
96
- end
97
- else
98
- #puts "DEBUG: COUNT_ON_HAND #{current_value.to_i}"
99
- load_object.on_hand = current_value.to_i
100
- end
101
-
102
- else
103
- super
104
- end
105
- end
106
-
107
- private
108
-
109
- # Special case for OptionTypes as it's two stage process
110
- # First add the possible option_types to Product, then we are able
111
- # to define Variants on those options values.
112
- #
113
- def add_options
114
- # TODO smart column ordering to ensure always valid by time we get to associations
115
- save_if_new
116
-
117
- option_types = current_value.split( LoaderBase::multi_assoc_delim )
118
-
119
- option_types.each do |ostr|
120
- oname, value_str = ostr.split(LoaderBase::name_value_delim)
121
-
122
- option_type = OptionType.find_by_name(oname)
123
-
124
- unless option_type
125
- option_type = OptionType.create( :name => oname, :presentation => oname.humanize)
126
- # TODO - dynamic creation should be an option
127
-
128
- unless option_type
129
- puts "WARNING: OptionType #{oname} NOT found - Not set Product"
130
- next
131
- end
132
- end
133
-
134
- @load_object.option_types << option_type unless @load_object.option_types.include?(option_type)
135
-
136
- # Can be simply list of OptionTypes, some or all without values
137
- next unless(value_str)
138
-
139
- # Now get the value(s) for the option e.g red,blue,green for OptType 'colour'
140
- ovalues = value_str.split(',')
141
-
142
- ovalues.each_with_index do |ovname, i|
143
- ovname.strip!
144
- ov = OptionValue.find_or_create_by_name(ovname)
145
- if ov
146
- object = Variant.create( :product => @load_object, :sku => "#{@load_object.sku}_#{i}", :price => @load_object.price, :available_on => @load_object.available_on)
147
- #puts "DEBUG: Create New Variant: #{object.inspect}"
148
- object.option_values << ov
149
- #@load_object.variants << object
150
- else
151
- puts "WARNING: Option #{ovname} NOT FOUND - No Variant created"
152
- end
153
- end
154
- end
155
-
156
- end
157
-
158
- # Special case for Images
159
- # A list of paths to Images with a optional 'alt' value - supplied in form :
160
- # path:alt|path2:alt2|path_3:alt3 etc
161
- #
162
- def add_images
163
- # TODO smart column ordering to ensure always valid by time we get to associations
164
- save_if_new
165
-
166
- images = current_value.split(LoaderBase::multi_assoc_delim)
167
-
168
- images.each do |image|
169
-
170
- img_path, alt_text = image.split(LoaderBase::name_value_delim)
171
-
172
- image = create_image(img_path, @load_object, :alt => alt_text)
173
- end
174
-
175
- end
176
-
177
-
178
- # Special case for ProductProperties since it can have additional value applied.
179
- # A list of Properties with a optional Value - supplied in form :
180
- # property.name:value|property.name|property.name:value
181
- #
182
- def add_properties
183
- # TODO smart column ordering to ensure always valid by time we get to associations
184
- save_if_new
185
-
186
- property_list = current_value.split(LoaderBase::multi_assoc_delim)
187
-
188
- property_list.each do |pstr|
189
- pname, pvalue = pstr.split(LoaderBase::name_value_delim)
190
- property = Property.find_by_name(pname)
191
-
192
- unless property
193
- property = Property.create( :name => pname, :presentation => pname.humanize)
194
- end
195
-
196
- if(property)
197
- @load_object.product_properties << ProductProperty.create( :property => property, :value => pvalue)
198
- else
199
- puts "WARNING: Property #{pname} NOT found - Not set Product"
200
- end
201
-
202
- end
203
-
204
- end
205
-
206
-
207
- def add_taxons
208
- # TODO smart column ordering to ensure always valid by time we get to associations
209
- save_if_new
210
-
211
- name_list = current_value.split(LoaderBase::multi_assoc_delim)
212
-
213
- taxons = name_list.collect do |t|
214
-
215
- taxon = Taxon.find_by_name(t)
216
-
217
- unless taxon
218
- parent = Taxonomy.find_by_name(t)
219
-
220
- begin
221
- if(parent)
222
- # not sure this can happen but just incase we get a weird situation where we have
223
- # a taxonomy without a root named the same - create the child taxon we require
224
- taxon = Taxon.create(:name => t, :taxonomy_id => parent.id)
225
- else
226
- parent = Taxonomy.create!( :name => t )
227
-
228
- taxon = parent.root
229
- end
230
-
231
- rescue => e
232
- e.backtrace
233
- e.inspect
234
- puts "ERROR : Cannot assign Taxon ['#{t}'] to Product ['#{load_object.name}']"
235
- next
236
- end
237
- end
238
- taxon
239
- end
240
-
241
- taxons.compact!
242
-
243
- @load_object.taxons << taxons unless(taxons.empty?)
244
-
245
- end
246
-
247
- end
248
- end
1
+ # Copyright:: (c) Autotelik Media Ltd 2010
2
+ # Author :: Tom Statter
3
+ # Date :: Aug 2010
4
+ # License:: MIT ?
5
+ #
6
+ # Details:: Specific over-rides/additions to support Spree Products
7
+ #
8
+ require 'loader_base'
9
+ require 'csv_loader'
10
+ require 'excel_loader'
11
+ require 'image_loader'
12
+
13
+ module DataShift
14
+
15
+ module SpreeHelper
16
+
17
+ class ProductLoader < LoaderBase
18
+
19
+ include DataShift::CsvLoading
20
+ include DataShift::ExcelLoading
21
+ include DataShift::ImageLoading
22
+
23
+ # depending on version get_product_class should return us right class, namespaced or not
24
+
25
+ def initialize(product = nil)
26
+ super( SpreeHelper::get_product_class(), product, :instance_methods => true )
27
+
28
+ @@option_type_klass ||= SpreeHelper::get_spree_class('OptionType')
29
+ @@option_value_klass ||= SpreeHelper::get_spree_class('OptionValue')
30
+ @@property_klass ||= SpreeHelper::get_spree_class('Property')
31
+ @@product_property_klass ||= SpreeHelper::get_spree_class('ProductProperty')
32
+ @@taxonomy_klass ||= SpreeHelper::get_spree_class('Taxonomy')
33
+ @@taxon_klass ||= SpreeHelper::get_spree_class('Taxon')
34
+ @@variant_klass ||= SpreeHelper::get_spree_class('Variant')
35
+
36
+ raise "Failed to create Product for loading" unless @load_object
37
+ end
38
+
39
+ # Based on filename call appropriate loading function
40
+ # Currently supports :
41
+ # Excel/Open Office files saved as .xls
42
+ # CSV files
43
+ def perform_load( file_name, options = {} )
44
+
45
+ ext = File.extname(file_name)
46
+
47
+ if(ext == '.xls')
48
+ raise DataShift::BadRuby, "Please install and use JRuby for loading .xls files" unless(Guards::jruby?)
49
+ perform_excel_load(file_name, options)
50
+ elsif(ext == '.csv')
51
+ perform_csv_load(file_name, options)
52
+ else
53
+ raise DataShift::UnsupportedFileType, "#{ext} files not supported - Try CSV or OpenOffice/Excel .xls"
54
+ end
55
+ end
56
+
57
+ # Over ride base class process with some Spree::Product specifics
58
+ #
59
+ # What process a value string from a column, assigning value(s) to correct association on Product.
60
+ # Method map represents a column from a file and it's correlated Product association.
61
+ # Value string which may contain multiple values for a collection (has_many) association.
62
+ #
63
+ def process()
64
+
65
+ # Special cases for Products, generally where a simple one stage lookup won't suffice
66
+ # otherwise simply use default processing from base class
67
+ if(current_value && (@current_method_detail.operator?('variants') || @current_method_detail.operator?('option_types')) )
68
+
69
+ add_options
70
+
71
+ elsif(@current_method_detail.operator?('taxons') && current_value)
72
+
73
+ add_taxons
74
+
75
+ elsif(@current_method_detail.operator?('product_properties') && current_value)
76
+
77
+ add_properties
78
+
79
+ elsif(@current_method_detail.operator?('images') && current_value)
80
+
81
+ add_images
82
+
83
+ elsif(current_value && (@current_method_detail.operator?('count_on_hand') || @current_method_detail.operator?('on_hand')) )
84
+
85
+
86
+ # Unless we can save here, in danger of count_on_hand getting wiped out.
87
+ # If we set (on_hand or count_on_hand) on an unsaved object, during next subsequent save
88
+ # looks like some validation code or something calls Variant.on_hand= with 0
89
+ # If we save first, then our values seem to stick
90
+
91
+ # TODO smart column ordering to ensure always valid - if we always make it very last column might not get wiped ?
92
+ #
93
+ save_if_new
94
+
95
+
96
+ # Spree has some stock management stuff going on, so dont usually assign to column vut use
97
+ # on_hand and on_hand=
98
+ if(@load_object.variants.size > 0 && current_value.include?(LoaderBase::multi_assoc_delim))
99
+
100
+ #puts "DEBUG: COUNT_ON_HAND PER VARIANT",current_value.is_a?(String),
101
+
102
+ # Check if we processed Option Types and assign count per option
103
+ values = current_value.to_s.split(LoaderBase::multi_assoc_delim)
104
+
105
+ if(@load_object.variants.size == values.size)
106
+ @load_object.variants.each_with_index {|v, i| v.on_hand = values[i]; v.save; }
107
+ else
108
+ puts "WARNING: Count on hand entries did not match number of Variants - None Set"
109
+ end
110
+ else
111
+ puts "WARNING: Multiple count_on_hand values specified but no Variants/OptionTypes created" if(@load_object.variants.empty?)
112
+ load_object.on_hand = current_value.to_i
113
+ end
114
+
115
+ else
116
+ super
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ # Special case for OptionTypes as it's two stage process
123
+ # First add the possible option_types to Product, then we are able
124
+ # to define Variants on those options values.
125
+ #
126
+ def add_options
127
+
128
+ # TODO smart column ordering to ensure always valid by time we get to associations
129
+ save_if_new
130
+
131
+ option_types = current_value.split( LoaderBase::multi_assoc_delim )
132
+
133
+ option_types.each do |ostr|
134
+ oname, value_str = ostr.split(LoaderBase::name_value_delim)
135
+
136
+ option_type = @@option_type_klass.find_by_name(oname)
137
+
138
+ unless option_type
139
+ option_type = @@option_type_klass.create( :name => oname, :presentation => oname.humanize)
140
+ # TODO - dynamic creation should be an option
141
+
142
+ unless option_type
143
+ puts "WARNING: OptionType #{oname} NOT found - Not set Product"
144
+ next
145
+ end
146
+ end
147
+
148
+ @load_object.option_types << option_type unless @load_object.option_types.include?(option_type)
149
+
150
+ # Can be simply list of OptionTypes, some or all without values
151
+ next unless(value_str)
152
+
153
+ # Now get the value(s) for the option e.g red,blue,green for OptType 'colour'
154
+ ovalues = value_str.split(',')
155
+
156
+ # TODO .. benchmarking to find most efficient way to create these but ensure Product.variants list
157
+ # populated .. currently need to call reload to ensure this (seems reqd for Spree 1/Rails 3, wasn't required b4
158
+ ovalues.each_with_index do |ovname, i|
159
+ ovname.strip!
160
+ ov = @@option_value_klass.find_or_create_by_name(ovname)
161
+ if ov
162
+ variant = @@variant_klass.create( :product => @load_object, :sku => "#{@load_object.sku}_#{i}", :price => @load_object.price, :available_on => @load_object.available_on)
163
+ #puts "DEBUG: Created New Variant: #{variant.inspect}"
164
+ variant.option_values << ov
165
+ else
166
+ puts "WARNING: Option #{ovname} NOT FOUND - No Variant created"
167
+ end
168
+ end
169
+
170
+ #puts "DEBUG Load Object now has Variants : #{@load_object.variants.inspect}"
171
+ @load_object.reload
172
+ #puts "DEBUG Load Object now has Variants : #{@load_object.variants.inspect}"
173
+ end
174
+
175
+ end
176
+
177
+ # Special case for Images
178
+ # A list of paths to Images with a optional 'alt' value - supplied in form :
179
+ # path:alt|path2:alt2|path_3:alt3 etc
180
+ #
181
+ def add_images
182
+ # TODO smart column ordering to ensure always valid by time we get to associations
183
+ save_if_new
184
+
185
+ images = current_value.split(LoaderBase::multi_assoc_delim)
186
+
187
+ images.each do |image|
188
+
189
+ img_path, alt_text = image.split(LoaderBase::name_value_delim)
190
+
191
+ image = create_image(img_path, @load_object, :alt => alt_text)
192
+ end
193
+
194
+ end
195
+
196
+
197
+ # Special case for ProductProperties since it can have additional value applied.
198
+ # A list of Properties with a optional Value - supplied in form :
199
+ # property.name:value|property.name|property.name:value
200
+ #
201
+ def add_properties
202
+ # TODO smart column ordering to ensure always valid by time we get to associations
203
+ save_if_new
204
+
205
+ property_list = current_value.split(LoaderBase::multi_assoc_delim)
206
+
207
+ property_list.each do |pstr|
208
+ pname, pvalue = pstr.split(LoaderBase::name_value_delim)
209
+ property = @@property_klass.find_by_name(pname)
210
+
211
+ unless property
212
+ property = @@property_klass.create( :name => pname, :presentation => pname.humanize)
213
+ end
214
+
215
+ if(property)
216
+ @load_object.product_properties << @@product_property_klass.create( :property => property, :value => pvalue)
217
+ else
218
+ puts "WARNING: Property #{pname} NOT found - Not set Product"
219
+ end
220
+
221
+ end
222
+
223
+ end
224
+
225
+ # Nested tree structure support ..
226
+ #
227
+ # ... inside of main loop
228
+ # the_taxons = []
229
+ # taxon_col.split(/[\r\n]+/).each do |chain|
230
+ # taxon = nil
231
+ # names = chain.split(/\s*>\s*/)
232
+ # names.each do |name|
233
+ # taxon = Taxon.find_or_create_by_name_and_parent_id_and_taxonomy_id(name, taxon && taxon.id, main_taxonomy.id)
234
+ # end
235
+ # the_taxons << taxon
236
+ # end
237
+ # p.taxons = the_taxons
238
+
239
+
240
+ # TAXON FORMAT
241
+ # name|name>child>child|name
242
+
243
+ def add_taxons
244
+ # TODO smart column ordering to ensure always valid by time we get to associations
245
+ save_if_new
246
+
247
+ chain_list = current_value().split(LoaderBase::multi_assoc_delim)
248
+
249
+ chain_list.each do |chain|
250
+
251
+ name_list = chain.split(/\s*>\s*/)
252
+
253
+ # manage per chain
254
+ parent_taxonomy, parent, taxon = nil, nil, nil
255
+
256
+ # Each chain can contain either a single Taxon, or the tree like structure parent>child>child
257
+ taxons = name_list.collect do |name|
258
+
259
+ #puts "DEBUG: NAME #{name.inspect}"
260
+ begin
261
+ taxon = @@taxon_klass.find_by_name( name )
262
+
263
+ if(taxon)
264
+ parent_taxonomy ||= taxon.taxonomy
265
+ else
266
+ parent_taxonomy ||= @@taxonomy_klass.find_or_create_by_name(name)
267
+
268
+ taxon = @@taxon_klass.find_or_create_by_name_and_parent_id_and_taxonomy_id(name, parent && parent.id, parent_taxonomy.id)
269
+ end
270
+ rescue => e
271
+ puts e.inspect
272
+ puts "ERROR : Cannot assign Taxon ['#{taxon}'] to Product ['#{load_object.name}']"
273
+ next
274
+ end
275
+
276
+ parent = taxon
277
+ taxon
278
+ end
279
+
280
+ unique_list = taxons.compact.uniq - (@load_object.taxons || [])
281
+
282
+ #puts "DEBUG: Taxon nms to add #{unique_list.collect(&:name).inspect}"
283
+ @load_object.taxons << unique_list unless(unique_list.empty?)
284
+ end
285
+
286
+ end
287
+
288
+ end
289
+ end
249
290
  end