datashift 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. data/.document +5 -0
  2. data/Gemfile +25 -0
  3. data/Gemfile.lock +211 -0
  4. data/LICENSE.txt +27 -0
  5. data/README.markdown +286 -0
  6. data/README.rdoc +19 -0
  7. data/Rakefile +96 -0
  8. data/VERSION +5 -0
  9. data/bin/autospec +16 -0
  10. data/bin/convert_to_should_syntax +16 -0
  11. data/bin/erubis +16 -0
  12. data/bin/htmldiff +16 -0
  13. data/bin/jeweler +16 -0
  14. data/bin/ldiff +16 -0
  15. data/bin/nokogiri +16 -0
  16. data/bin/rackup +16 -0
  17. data/bin/rails +16 -0
  18. data/bin/rake +16 -0
  19. data/bin/rake2thor +16 -0
  20. data/bin/ri +16 -0
  21. data/bin/rspec +16 -0
  22. data/bin/spree +16 -0
  23. data/bin/thor +16 -0
  24. data/bin/tilt +16 -0
  25. data/bin/tt +16 -0
  26. data/datashift.gemspec +178 -0
  27. data/lib/applications/jruby/jexcel_file.rb +397 -0
  28. data/lib/applications/jruby/word.rb +79 -0
  29. data/lib/datashift.rb +114 -0
  30. data/lib/datashift/exceptions.rb +12 -0
  31. data/lib/datashift/file_definitions.rb +353 -0
  32. data/lib/datashift/mapping_file_definitions.rb +88 -0
  33. data/lib/datashift/method_detail.rb +237 -0
  34. data/lib/datashift/method_mapper.rb +257 -0
  35. data/lib/generators/csv_generator.rb +36 -0
  36. data/lib/generators/excel_generator.rb +122 -0
  37. data/lib/generators/generator_base.rb +14 -0
  38. data/lib/helpers/core_ext/to_b.rb +24 -0
  39. data/lib/helpers/spree_helper.rb +131 -0
  40. data/lib/java/poi-3.7/._poi-3.7-20101029.jar5645100390082102460.tmp +0 -0
  41. data/lib/java/poi-3.7/LICENSE +507 -0
  42. data/lib/java/poi-3.7/NOTICE +21 -0
  43. data/lib/java/poi-3.7/RELEASE_NOTES.txt +115 -0
  44. data/lib/java/poi-3.7/lib/commons-logging-1.1.jar +0 -0
  45. data/lib/java/poi-3.7/lib/junit-3.8.1.jar +0 -0
  46. data/lib/java/poi-3.7/lib/log4j-1.2.13.jar +0 -0
  47. data/lib/java/poi-3.7/ooxml-lib/dom4j-1.6.1.jar +0 -0
  48. data/lib/java/poi-3.7/ooxml-lib/geronimo-stax-api_1.0_spec-1.0.jar +0 -0
  49. data/lib/java/poi-3.7/ooxml-lib/xmlbeans-2.3.0.jar +0 -0
  50. data/lib/java/poi-3.7/poi-3.7-20101029.jar +0 -0
  51. data/lib/java/poi-3.7/poi-examples-3.7-20101029.jar +0 -0
  52. data/lib/java/poi-3.7/poi-ooxml-3.7-20101029.jar +0 -0
  53. data/lib/java/poi-3.7/poi-ooxml-schemas-3.7-20101029.jar +0 -0
  54. data/lib/java/poi-3.7/poi-scratchpad-3.7-20101029.jar +0 -0
  55. data/lib/loaders/csv_loader.rb +99 -0
  56. data/lib/loaders/excel_loader.rb +150 -0
  57. data/lib/loaders/loader_base.rb +332 -0
  58. data/lib/loaders/spreadsheet_loader.rb +137 -0
  59. data/lib/loaders/spree/image_loader.rb +46 -0
  60. data/lib/loaders/spree/product_loader.rb +225 -0
  61. data/spec/csv_loader_spec.rb +31 -0
  62. data/spec/datashift_spec.rb +27 -0
  63. data/spec/db/migrate/20110803201325_create_test_bed.rb +85 -0
  64. data/spec/excel_generator_spec.rb +79 -0
  65. data/spec/excel_loader_spec.rb +177 -0
  66. data/spec/file_definitions.rb +141 -0
  67. data/spec/fixtures/BadAssociationName.xls +0 -0
  68. data/spec/fixtures/DemoNegativeTesting.xls +0 -0
  69. data/spec/fixtures/ProjectsMultiCategories.xls +0 -0
  70. data/spec/fixtures/ProjectsSingleCategories.xls +0 -0
  71. data/spec/fixtures/SimpleProjects.xls +0 -0
  72. data/spec/fixtures/config/database.yml +25 -0
  73. data/spec/fixtures/interact_models_db.sqlite +0 -0
  74. data/spec/fixtures/interact_spree_db.sqlite +0 -0
  75. data/spec/fixtures/negative/SpreeProdMiss1Mandatory.csv +4 -0
  76. data/spec/fixtures/negative/SpreeProdMiss1Mandatory.xls +0 -0
  77. data/spec/fixtures/negative/SpreeProdMissManyMandatory.csv +4 -0
  78. data/spec/fixtures/negative/SpreeProdMissManyMandatory.xls +0 -0
  79. data/spec/fixtures/simple_export_spec.xls +0 -0
  80. data/spec/fixtures/simple_template_spec.xls +0 -0
  81. data/spec/fixtures/spree/SpreeProducts.csv +4 -0
  82. data/spec/fixtures/spree/SpreeProducts.xls +0 -0
  83. data/spec/fixtures/spree/SpreeProductsMultiColumn.csv +4 -0
  84. data/spec/fixtures/spree/SpreeProductsMultiColumn.xls +0 -0
  85. data/spec/fixtures/spree/SpreeProductsSimple.csv +4 -0
  86. data/spec/fixtures/spree/SpreeProductsSimple.xls +0 -0
  87. data/spec/fixtures/spree/SpreeZoneExample.csv +5 -0
  88. data/spec/fixtures/spree/SpreeZoneExample.xls +0 -0
  89. data/spec/fixtures/test_model_defs.rb +57 -0
  90. data/spec/loader_spec.rb +121 -0
  91. data/spec/method_mapper_spec.rb +238 -0
  92. data/spec/spec_helper.rb +116 -0
  93. data/spec/spree_generator_spec.rb +65 -0
  94. data/spec/spree_loader_spec.rb +311 -0
  95. data/spec/spree_method_mapping_spec.rb +215 -0
  96. data/tasks/config/seed_fu_product_template.erb +15 -0
  97. data/tasks/config/tidy_config.txt +13 -0
  98. data/tasks/db_tasks.rake +65 -0
  99. data/tasks/excel_generator.rake +79 -0
  100. data/tasks/file_tasks.rake +37 -0
  101. data/tasks/import/csv.rake +50 -0
  102. data/tasks/import/excel.rake +67 -0
  103. data/tasks/spree/image_load.rake +109 -0
  104. data/tasks/spree/product_loader.rake +44 -0
  105. data/tasks/word_to_seedfu.rake +167 -0
  106. data/test/helper.rb +18 -0
  107. data/test/test_interact.rb +7 -0
  108. metadata +301 -0
@@ -0,0 +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 "Excel 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
+ end
@@ -0,0 +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
+ module DataShift
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
+
46
+ end
@@ -0,0 +1,225 @@
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 'excel_loader'
10
+ require 'csv_loader'
11
+
12
+ module DataShift
13
+
14
+ module Spree
15
+
16
+ class ProductLoader < LoaderBase
17
+
18
+ include DataShift::CsvLoading
19
+ include DataShift::ExcelLoading
20
+
21
+ def initialize(product = nil)
22
+ super( Product, product, :instance_methods => true )
23
+ raise "Failed to create Product for loading" unless @load_object
24
+ end
25
+
26
+
27
+ # Based on filename call appropriate loading function
28
+ # Currently supports :
29
+ # Excel/Open Office files saved as .xls
30
+ # CSV files
31
+ def perform_load( file_name, options = {} )
32
+
33
+ ext = File.extname(file_name)
34
+
35
+ if(ext == '.xls')
36
+ raise DataShift::BadRuby, "Please install and use JRuby for loading .xls files" unless(Guards::jruby?)
37
+ perform_excel_load(file_name, options)
38
+ elsif(ext == '.csv')
39
+ perform_csv_load(file_name, options)
40
+ else
41
+ raise DataShift::UnsupportedFileType, "#{ext} files not supported - Try CSV or OpenOffice/Excel .xls"
42
+ end
43
+ end
44
+
45
+ # Over ride base class process with some Spree::Product specifics
46
+ #
47
+ # What process a value string from a column, assigning value(s) to correct association on Product.
48
+ # Method map represents a column from a file and it's correlated Product association.
49
+ # Value string which may contain multiple values for a collection (has_many) association.
50
+ #
51
+ def process()
52
+
53
+ # Special cases for Products, generally where a simple one stage lookup won't suffice
54
+ # otherwise simply use default processing from base class
55
+ if((@current_method_detail.operator?('variants') || @current_method_detail.operator?('option_types')) && current_value)
56
+
57
+ add_options
58
+
59
+ elsif(@current_method_detail.operator?('taxons') && current_value)
60
+
61
+ add_taxons
62
+
63
+ elsif(@current_method_detail.operator?('product_properties') && current_value)
64
+
65
+ add_properties
66
+
67
+ elsif(@current_method_detail.operator?('count_on_hand') || @current_method_detail.operator?('on_hand') )
68
+
69
+ # Unless we can save here, in danger of count_on_hand getting wiped out.
70
+ # If we set (on_hand or count_on_hand) on an unsaved object, during next subsequent save
71
+ # looks like some validation code or something calls Variant.on_hand= with 0
72
+ # If we save first, then our values seem to stick
73
+
74
+ # TODO smart column ordering to ensure always valid - if we always make it very last column might not get wiped ?
75
+ #
76
+ save_if_new
77
+
78
+ # Spree has some stock management stuff going on, so dont usually assign to column vut use
79
+ # on_hand and on_hand=
80
+ if(@load_object.variants.size > 0 && current_value.include?(LoaderBase::multi_assoc_delim))
81
+
82
+ #puts "DEBUG: COUNT_ON_HAND PER VARIANT",current_value.is_a?(String),
83
+ #&& current_value.is_a?(String) && current_value.include?(LoaderBase::multi_assoc_delim))
84
+ # Check if we processed Option Types and assign count per option
85
+ values = current_value.to_s.split(LoaderBase::multi_assoc_delim)
86
+
87
+ if(@load_object.variants.size == values.size)
88
+ @load_object.variants.each_with_index {|v, i| v.on_hand = values[i]; v.save; }
89
+ else
90
+ puts "WARNING: Count on hand entries did not match number of Variants - None Set"
91
+ end
92
+ else
93
+ #puts "DEBUG: COUNT_ON_HAND #{current_value.to_i}"
94
+ load_object.on_hand = current_value.to_i
95
+ end
96
+
97
+ else
98
+ super
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ # Special case for OptionTypes as it's two stage process
105
+ # First add the possible option_types to Product, then we are able
106
+ # to define Variants on those options values.
107
+ #
108
+ def add_options
109
+ # TODO smart column ordering to ensure always valid by time we get to associations
110
+ save_if_new
111
+
112
+ option_types = current_value.split( LoaderBase::multi_assoc_delim )
113
+
114
+ option_types.each do |ostr|
115
+ oname, value_str = ostr.split(LoaderBase::name_value_delim)
116
+
117
+ option_type = OptionType.find_by_name(oname)
118
+
119
+ unless option_type
120
+ option_type = OptionType.create( :name => oname, :presentation => oname.humanize)
121
+ # TODO - dynamic creation should be an option
122
+
123
+ unless option_type
124
+ puts "WARNING: OptionType #{oname} NOT found - Not set Product"
125
+ next
126
+ end
127
+ end
128
+
129
+ @load_object.option_types << option_type unless @load_object.option_types.include?(option_type)
130
+
131
+ # Can be simply list of OptionTypes, some or all without values
132
+ next unless(value_str)
133
+
134
+ # Now get the value(s) for the option e.g red,blue,green for OptType 'colour'
135
+ ovalues = value_str.split(',')
136
+
137
+ ovalues.each_with_index do |ovname, i|
138
+ ovname.strip!
139
+ ov = OptionValue.find_or_create_by_name(ovname)
140
+ if ov
141
+ object = Variant.create( :product => @load_object, :sku => "#{@load_object.sku}_#{i}", :price => @load_object.price, :available_on => @load_object.available_on)
142
+ #puts "DEBUG: Create New Variant: #{object.inspect}"
143
+ object.option_values << ov
144
+ #@load_object.variants << object
145
+ else
146
+ puts "WARNING: Option #{ovname} NOT FOUND - No Variant created"
147
+ end
148
+ end
149
+ end
150
+
151
+ end
152
+
153
+
154
+ # Special case for ProductProperties since it can have additional value applied.
155
+ # A list of Properties with a optional Value - supplied in form :
156
+ # property.name:value|property.name|property.name:value
157
+ #
158
+ def add_properties
159
+ # TODO smart column ordering to ensure always valid by time we get to associations
160
+ save_if_new
161
+
162
+ property_list = current_value.split(LoaderBase::multi_assoc_delim)
163
+
164
+ property_list.each do |pstr|
165
+ pname, pvalue = pstr.split(LoaderBase::name_value_delim)
166
+ property = Property.find_by_name(pname)
167
+
168
+ unless property
169
+ property = Property.create( :name => pname, :presentation => pname.humanize)
170
+ end
171
+
172
+ if(property)
173
+ @load_object.product_properties << ProductProperty.create( :property => property, :value => pvalue)
174
+ else
175
+ puts "WARNING: Property #{pname} NOT found - Not set Product"
176
+ end
177
+
178
+ end
179
+
180
+ end
181
+
182
+
183
+ def add_taxons
184
+ # TODO smart column ordering to ensure always valid by time we get to associations
185
+ save_if_new
186
+
187
+ name_list = current_value.split(LoaderBase::multi_assoc_delim)
188
+
189
+ taxons = name_list.collect do |t|
190
+
191
+ taxon = Taxon.find_by_name(t)
192
+
193
+ unless taxon
194
+ parent = Taxonomy.find_by_name(t)
195
+
196
+ begin
197
+ if(parent)
198
+ # not sure this can happen but just incase we get a weird situation where we have
199
+ # a taxonomy without a root named the same - create the child taxon we require
200
+ taxon = Taxon.create(:name => t, :taxonomy_id => parent.id)
201
+ else
202
+ parent = Taxonomy.create!( :name => t )
203
+
204
+ taxon = parent.root
205
+ end
206
+
207
+ rescue => e
208
+ e.backtrace
209
+ e.inspect
210
+ puts "ERROR : Cannot assign Taxon ['#{t}'] to Product ['#{load_object.name}']"
211
+ next
212
+ end
213
+ end
214
+ taxon
215
+ end
216
+
217
+ taxons.compact!
218
+
219
+ @load_object.taxons << taxons unless(taxons.empty?)
220
+
221
+ end
222
+
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,31 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2011
2
+ # Author :: Tom Statter
3
+ # Date :: Aug 2011
4
+ # License:: MIT
5
+ #
6
+ # Details:: Specs for Excel aspect of Active Record Loader
7
+ #
8
+ require File.dirname(__FILE__) + '/spec_helper'
9
+
10
+ require 'erb'
11
+ require 'excel_loader'
12
+
13
+ describe 'CSV Loader' do
14
+
15
+ before(:all) do
16
+
17
+ # load our test model definitions - Project etc
18
+ require File.join($DataShiftFixturePath, 'test_model_defs')
19
+
20
+ db_connect( 'test_file' ) # , test_memory, test_mysql
21
+ migrate_up
22
+ @klazz = Project
23
+ end
24
+
25
+ before(:each) do
26
+ MethodMapper.clear
27
+ MethodMapper.find_operators( @klazz )
28
+ MethodMapper.find_operators( @assoc_klazz )
29
+ end
30
+
31
+ end
@@ -0,0 +1,27 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2011
2
+ # Author :: Tom Statter
3
+ # Date :: Oct 2011
4
+ # License:: MIT
5
+ #
6
+ # Details:: Specs for high level apsects of DataShift library
7
+ #
8
+ require File.dirname(__FILE__) + '/spec_helper'
9
+
10
+ describe 'DataShift' do
11
+
12
+ before(:each) do
13
+ end
14
+
15
+ it "should provide gem version" do
16
+ DataShift::gem_version
17
+ end
18
+
19
+ it "should provide gem name" do
20
+ DataShift::gem_name.should == 'datashift'
21
+ end
22
+
23
+ it "should provide root_path" do
24
+ DataShift.root_path.should_not be_empty
25
+ end
26
+
27
+ end