datashift 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,29 +2,31 @@
2
2
 
3
3
  Provides tools to shift data between Ruby, external applications, files and ActiveRecord.
4
4
 
5
- Wiki taking shape with more info here : <b>https://github.com/autotelik/datashift/wiki</b>
5
+ Specific loaders and command line tasks for Spree E-Commerce.
6
6
 
7
- ### Features
7
+ Wiki taking shape with more info here : **https://github.com/autotelik/datashift/wiki**
8
8
 
9
- Map Active Record models and associations to files, generate sample templates.
9
+ ### Features
10
10
 
11
11
  Import and Export ActiveRecord models through .xls or CSV files, including
12
12
  all associations and with configurable defaults.
13
13
 
14
+ Export all data or simply generate a sample template with headers only.
15
+
14
16
  Create, parse and use Excel/OpenOffice (.xls) documents dynamically from Ruby.
15
17
 
16
18
  Easily extendable Loader functionality to deal with non trivial import cases, such
17
19
  as complex association lookups.
18
20
 
19
- High level rake/thor command line tasks for import/export provided.
21
+ High level rake and thor command line tasks for import/export provided.
20
22
 
21
- Specific loaders and rake tasks for Spree E-Commerce, enabling import/export of all data including Products with
22
- complex associations such as Properties/Taxons/Options/Variants and Images.
23
+ Specific loaders and command line tasks provided out the box for **Spree E-Commerce**,
24
+ enabling import/export of Product data including creating Variants with different
25
+ count on hands and all associations including Properties/Taxons/OptionTypes and Images.
23
26
 
24
- Specific loaders and rake tasks provided out the box for Spree E-Commerce,
25
- enabling import/export of all data including Products with complex associations such as Options/Variants and Images.
27
+ Loaders can be configured via YAML with over ride values, default values and mandatory column settingss.
26
28
 
27
- Many example Spreadsheets in spec/fixtures, fully documented with comments for each column.
29
+ Many example Spreadsheets/CSV files in spec/fixtures, fully documented with comments for each column.
28
30
 
29
31
  ## Installation
30
32
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.0
1
+ 0.6.1
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "datashift"
8
- s.version = "0.6.0"
8
+ s.version = "0.6.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Thomas Statter"]
12
- s.date = "2012-05-18"
12
+ s.date = "2012-05-24"
13
13
  s.description = "A suite of tools to move data between ActiveRecord models,databases,applications like Excel/Open Office, files and projects including Spree"
14
14
  s.email = "rubygems@autotelik.co.uk"
15
15
  s.extra_rdoc_files = [
@@ -151,7 +151,6 @@ Gem::Specification.new do |s|
151
151
  "tasks/file_tasks.rake",
152
152
  "tasks/import/csv.rake",
153
153
  "tasks/import/excel.rake",
154
- "tasks/spree/image_load.rake",
155
154
  "tasks/word_to_seedfu.rake",
156
155
  "test/helper.rb",
157
156
  "test/test_interact.rb"
@@ -124,6 +124,8 @@ module DataShift
124
124
  end
125
125
  end
126
126
  puts "Excel loading stage complete - #{loaded_objects.size} rows added."
127
+
128
+ puts "WARNING : #{failed_objects.size} rows contained errors and #{failed_objects.size} records NOT created." unless failed_objects.empty?
127
129
  end
128
130
 
129
131
  def value_at(row, column)
@@ -100,6 +100,7 @@ module DataShift
100
100
 
101
101
  @method_mapper = DataShift::MethodMapper.new
102
102
  @options = options.clone
103
+ @verbose_logging = @options[:verbose_logging]
103
104
  @headers = []
104
105
 
105
106
  @default_data_objects ||= {}
@@ -216,7 +217,11 @@ module DataShift
216
217
 
217
218
  # Default values can be provided in YAML config file
218
219
  # Format :
219
- # Load Class
220
+ #
221
+ # LoaderClass:
222
+ # option: value
223
+ #
224
+ # Load Class: (e.g Spree:Product)
220
225
  # atttribute: value
221
226
 
222
227
  def configure_from( yaml_file )
@@ -251,6 +256,8 @@ module DataShift
251
256
 
252
257
  if(data[load_object_class.name])
253
258
 
259
+ logger.info("Assigning defaults and over rides from config")
260
+
254
261
  deflts = data[load_object_class.name]['datashift_defaults']
255
262
  @default_values.merge!(deflts) if deflts
256
263
 
@@ -258,6 +265,11 @@ module DataShift
258
265
  @override_values.merge!(ovrides) if ovrides
259
266
  end
260
267
 
268
+ if(data['LoaderBase'])
269
+ @options.merge!(data['LoaderBase'])
270
+ end
271
+
272
+ logger.info("Loader Options : #{@options.inspect}")
261
273
  end
262
274
 
263
275
  # Set member variables to hold details and value.
@@ -285,13 +297,28 @@ module DataShift
285
297
  @current_value
286
298
  end
287
299
 
288
-
300
+ # return the find_by operator and the values to find
301
+ def get_find_operator_and_rest( column_data)
302
+
303
+ find_operator, col_values = "",nil
304
+
305
+ if(@current_method_detail.find_by_operator)
306
+ find_operator, col_values = @current_method_detail.find_by_operator, column_data
307
+ else
308
+ find_operator, col_values = column_data.split(LoaderBase::name_value_delim)
309
+ end
310
+
311
+ return find_operator, col_values
312
+ end
313
+
289
314
  # Process a value string from a column.
290
315
  # Assigning value(s) to correct association on @load_object.
291
316
  # Method detail represents a column from a file and it's correlated AR associations.
292
317
  # Value string which may contain multiple values for a collection association.
293
318
  #
294
- def process()
319
+ def process()
320
+
321
+ logger.info("Current value to assign : #{@current_value}") #if @options['verboose_logging']
295
322
 
296
323
  if(@current_method_detail.operator_for(:has_many))
297
324
 
@@ -310,17 +337,18 @@ module DataShift
310
337
 
311
338
  columns.each do |col_str|
312
339
 
313
- find_operator, col_values = "",""
314
-
315
- if(@current_method_detail.find_by_operator)
316
- find_operator, col_values = @current_method_detail.find_by_operator, col_str
317
- else
318
- find_operator, col_values = col_str.split(LoaderBase::name_value_delim)
319
- raise "No key to find #{@current_method_detail.operator} in DB. Expected format key:value" unless(col_values)
320
- end
340
+ find_operator, col_values = get_find_operator_and_rest( col_str )
321
341
 
342
+ #if(@current_method_detail.find_by_operator)
343
+ # find_operator, col_values = @current_method_detail.find_by_operator, col_str
344
+ # else
345
+ # find_operator, col_values = col_str.split(LoaderBase::name_value_delim)
346
+ # end
347
+
348
+ raise "Cannot perform DB find by #{find_operator}. Expected format key:value" unless(find_operator && col_values)
349
+
322
350
  find_by_values = col_values.split(LoaderBase::multi_value_delim)
323
-
351
+
324
352
  if(find_by_values.size > 1)
325
353
 
326
354
  @current_value = @current_method_detail.operator_class.send("find_all_by_#{find_operator}", find_by_values )
@@ -215,8 +215,10 @@ module DataShift
215
215
 
216
216
  # Special case for ProductProperties since it can have additional value applied.
217
217
  # A list of Properties with a optional Value - supplied in form :
218
- # property.name:value|property.name|property.name:value
219
- #
218
+ # property_name:value|property_name|property_name:value
219
+ # Example :
220
+ # test_pp_002|test_pp_003:Example free value|yet_another_property
221
+
220
222
  def add_properties
221
223
  # TODO smart column ordering to ensure always valid by time we get to associations
222
224
  save_if_new
@@ -224,27 +226,31 @@ module DataShift
224
226
  property_list = get_each_assoc#current_value.split(LoaderBase::multi_assoc_delim)
225
227
 
226
228
  property_list.each do |pstr|
227
- pname, pvalue = pstr.split(LoaderBase::name_value_delim)
228
- property = @@property_klass.find_by_name(pname)
229
+
230
+ find_by_name, find_by_value = get_find_operator_and_rest( pstr )
231
+
232
+ raise "Cannot find Property via #{find_by_name} (with value #{find_by_value})" unless(find_by_name)
233
+
234
+ property = @@property_klass.find_by_name(find_by_name)
229
235
 
230
236
  unless property
231
- property = @@property_klass.create( :name => pname, :presentation => pname.humanize)
232
- logger.debug "Created New Property #{property.inspect}"
237
+ property = @@property_klass.create( :name => find_by_name, :presentation => find_by_name.humanize)
238
+ logger.info "Created New Property #{property.inspect}"
233
239
  end
234
240
 
235
241
  if(property)
236
242
  if(SpreeHelper::version.to_f >= 1.1)
237
243
  # Property now protected from mass assignment
238
- x = @@product_property_klass.new( :value => pvalue )
244
+ x = @@product_property_klass.new( :value => find_by_value )
239
245
  x.property = property
240
246
  x.save
241
- logger.debug "Created New ProductProperty #{x.inspect}"
247
+ logger.info "Created New ProductProperty #{x.inspect}"
242
248
  @load_object.product_properties << x
243
249
  else
244
- @load_object.product_properties << @@product_property_klass.create( :property => property, :value => pvalue)
250
+ @load_object.product_properties << @@product_property_klass.create( :property => property, :value => find_by_values)
245
251
  end
246
252
  else
247
- puts "WARNING: Property #{pname} NOT found - Not set Product"
253
+ puts "WARNING: Property #{find_by_name} NOT found - Not set Product"
248
254
  end
249
255
 
250
256
  end
@@ -16,27 +16,37 @@ module Datashift
16
16
 
17
17
  desc "cleanup", "Remove Spree Product/Variant data from DB"
18
18
 
19
- def cleanup() #, [:input, :verbose, :sku_prefix] => :environment do |t, args|
19
+ def cleanup()
20
20
 
21
- require 'spree_helper'
21
+ require 'spree_helper'
22
22
 
23
23
  require File.expand_path('config/environment.rb')
24
-
25
- %w{Image OptionType OptionValue Product Property ProductGroup ProductProperty ProductOptionType Variant Taxonomy Taxon Zone}.each do |k|
26
- instance_variable_set("@#{k}_klass", DataShift::SpreeHelper::get_spree_class(k))
27
- puts "Clearing model #{DataShift::SpreeHelper::get_spree_class(k)}"
28
- instance_variable_get("@#{k}_klass").delete_all
24
+
25
+ cleanup = %w{ Image OptionType OptionValue
26
+ Product Property ProductGroup ProductProperty ProductOptionType
27
+ Variant Taxonomy Taxon Zone
28
+ }
29
+
30
+ cleanup.each do |k|
31
+ klass = DataShift::SpreeHelper::get_spree_class(k)
32
+ if(klass)
33
+ puts "Clearing model #{klass}"
34
+ klass.delete_all
35
+ else
36
+ puts "WARNING - Coulod not find AR model for class name #{k}"
29
37
  end
38
+
39
+ end
30
40
 
31
- if(File.exists?('public/spree/products') )
32
- puts "Removing old Product assets from 'public/spree/products'"
33
- FileUtils::rm_rf('public/public/spree/products')
34
- end
41
+ if(File.exists?('public/spree/products') )
42
+ puts "Removing old Product assets from 'public/spree/products'"
43
+ FileUtils::rm_rf('public/public/spree/products')
44
+ end
35
45
 
36
- FileUtils::rm_rf('MissingRecords') if(File.exists?('MissingRecords') )
46
+ FileUtils::rm_rf('MissingRecords') if(File.exists?('MissingRecords') )
37
47
 
38
- end
39
-
40
48
  end
49
+
50
+ end
41
51
 
42
52
  end
@@ -75,24 +75,27 @@ module Datashift
75
75
  method_option :sku_prefix, :aliases => '-p', :desc => "Prefix to add to each SKU in import file"
76
76
  method_option :dummy, :aliases => '-d', :type => :boolean, :desc => "Dummy run, do not actually save Image or Product"
77
77
  method_option :verbose, :aliases => '-v', :type => :boolean, :desc => "Verbose logging"
78
- #method_option :config, :aliases => '-c', :type => :string, :desc => "Configuration file containg defaults or over rides in YAML"
78
+ method_option :config, :aliases => '-c', :type => :string, :desc => "Configuration file for Image Loader in YAML"
79
+
79
80
 
80
81
  def images()
81
82
 
82
83
  require File.expand_path('config/environment.rb')
83
84
 
84
85
  require 'image_loader'
85
-
86
- @image_cache = options[:input]
87
86
 
88
87
  puts "Using Product Name for lookup" unless(options[:sku])
89
88
  puts "Using SKU for lookup" if(options[:sku])
90
-
91
-
89
+
92
90
  attachment_klazz = DataShift::SpreeHelper::get_spree_class('Product' )
93
- sku_klazz = DataShift::SpreeHelper::get_spree_class('Variant' )
91
+ attachment_field = 'name'
94
92
  image_klazz = DataShift::SpreeHelper::get_spree_class('Image' )
95
93
 
94
+ if(options[:sku])
95
+ attachment_klazz = DataShift::SpreeHelper::get_spree_class('Variant' )
96
+ attachment_field = 'sku'
97
+ end
98
+
96
99
  # TODO generalise for any paperclip project, for now just Spree
97
100
  #begin
98
101
  # attachment_klazz = Kernel.const_get(args[:model]) if(args[:model])
@@ -102,6 +105,22 @@ module Datashift
102
105
 
103
106
  image_loader = DataShift::SpreeHelper::ImageLoader.new
104
107
 
108
+ @loader_config = {}
109
+
110
+ if(options[:config])
111
+ raise "Bad Config - Cannot find specified file #{options[:config]}" unless File.exists?(options[:config])
112
+
113
+ image_loader.configure_from( options[:config] )
114
+
115
+ @loader_config = YAML::load( File.open(options[:config]) )
116
+
117
+ @loader_config = @loader_config['ImageLoader']
118
+ end
119
+
120
+ puts "CONFIG: #{@loader_config.inspect}"
121
+
122
+ @image_cache = options[:input]
123
+
105
124
  if(File.directory? @image_cache )
106
125
  logger.info "Loading Spree images from #{@image_cache}"
107
126
 
@@ -109,32 +128,23 @@ module Datashift
109
128
  Dir.glob("#{@image_cache}/**/*.{jpg,png,gif}") do |image_name|
110
129
 
111
130
  base_name = File.basename(image_name, '.*')
112
-
113
- logger.info "Processing #{base_name} : #{File.exists?(image_name)}"
131
+ base_name.strip!
132
+
133
+ logger.info "Processing image #{base_name} : #{File.exists?(image_name)}"
114
134
 
115
135
  record = nil
116
- if(options[:sku])
117
- sku = base_name.slice!(/\w+/)
118
- sku.strip!
119
- base_name.strip!
120
-
121
- sku = "#{options[:sku_prefix]}#{sku}" if(options[:sku_prefix])
122
-
123
- record = sku_klazz.find_by_sku(sku)
124
-
125
- unless record # try splitting up filename in various ways looking for the SKU
126
- sku.split( '_' ).each do |x|
127
- x = "#{options[:sku_prefix]}#{x}" if(options[:sku_prefix])
128
- record = sku_klazz.find_by_sku(x)
129
- break if record
130
- end
131
- end
132
-
133
- record = record.product if(record) # SKU stored on Variant but we want it's master Product
134
-
135
- else
136
- record = attachment_klazz.find_by_name(base_name)
136
+
137
+ put "Processing image [#{base_name}]"
138
+
139
+ # unless record # try splitting up filename in various ways looking for the SKU
140
+ split_on = @loader_config[:split_file_name_on] || '_'
141
+
142
+ base_name.split(split_on).each do |x|
143
+ record = get_record_by(attachment_klazz, attachment_field, x)
144
+ break if record
137
145
  end
146
+
147
+ record = record.product if(record) # SKU stored on Variant but we want it's master Product
138
148
 
139
149
  if(record)
140
150
  logger.info "Found record for attachment : #{record.inspect}"
@@ -175,6 +185,20 @@ module Datashift
175
185
  exit(-1)
176
186
  end
177
187
  end
188
+
189
+ private
190
+
191
+ def get_record_by(klazz, field, value)
192
+ x = (options[:sku_prefix]) ? "#{options[:sku_prefix]}#{value}" : value
193
+
194
+ if(@loader_config['case_sensitive'])
195
+ puts "Search case sensitive for [#{x}] on #{field}"
196
+ return klazz.find(:first, :conditions => [ "? = ?", field, x ])
197
+ else
198
+ puts "Search for [#{x}] on #{field}"
199
+ return klazz.find(:first, :conditions => [ "lower(?) = ?", field, x.downcase ])
200
+ end
201
+ end
178
202
  end
179
203
 
180
204
  end
@@ -83,11 +83,11 @@ describe 'SpreeLoader' do
83
83
 
84
84
  # Loader should perform identically regardless of source, whether csv, .xls etc
85
85
 
86
- it "should load basic Products .xls via Spree loader" do
86
+ it "should load basic Products .xls via Spree loader", :fail => true do
87
87
  test_basic_product('SpreeProductsSimple.xls')
88
88
  end
89
89
 
90
- it "should load basic Products from .csv via Spree loader", :csv => true do
90
+ it "should load basic Products from .csv via Spree loader", :csv => true, :fail => true do
91
91
  test_basic_product('SpreeProductsSimple.csv')
92
92
  end
93
93
 
@@ -104,6 +104,10 @@ describe 'SpreeLoader' do
104
104
  @product_loader.perform_load( SpecHelper::spree_fixture(source), :mandatory => ['sku', 'name', 'price'] )
105
105
 
106
106
  @Product_klass.count.should == 3
107
+
108
+ # 2 products available_on set in past, 1 in future
109
+ @Product_klass.active.size.should == 2
110
+ @Product_klass.available.size.should == 2
107
111
 
108
112
  @product_loader.failed_objects.size.should == 0
109
113
  @product_loader.loaded_objects.size.should == 3
@@ -179,7 +183,7 @@ describe 'SpreeLoader' do
179
183
  end
180
184
 
181
185
 
182
- it "should load Products and create Variants from multiple column", :fail => true do
186
+ it "should load Products and create Variants from multiple column #{SpecHelper::spree_fixture('SpreeProductsMultiColumn.xls')}", :fail => true do
183
187
  test_variants_creation('SpreeProductsMultiColumn.xls')
184
188
  end
185
189
 
@@ -28,5 +28,8 @@ describe 'Thor high level command line tasks' do
28
28
  Datashift::Spree.start(["products"])
29
29
  end
30
30
 
31
-
31
+ it "should be able to run an export to excel from a simple command line task" do
32
+ Datashift::Spree.start(["products"])
33
+ end
34
+
32
35
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: datashift
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.6.0
5
+ version: 0.6.1
6
6
  platform: ruby
7
7
  authors:
8
8
  - Thomas Statter
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2012-05-18 00:00:00 Z
13
+ date: 2012-05-24 00:00:00 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: spreadsheet
@@ -167,7 +167,6 @@ files:
167
167
  - tasks/file_tasks.rake
168
168
  - tasks/import/csv.rake
169
169
  - tasks/import/excel.rake
170
- - tasks/spree/image_load.rake
171
170
  - tasks/word_to_seedfu.rake
172
171
  - test/helper.rb
173
172
  - test/test_interact.rb
@@ -1,108 +0,0 @@
1
- # Copyright:: (c) Autotelik Media Ltd 2011
2
- # Author :: Tom Statter
3
- # Date :: Feb 2011
4
- # License:: MIT
5
- #
6
- # Usage::
7
- #
8
- # => rake datashift:spree:images input=vendor/extensions/site/fixtures/images
9
- # => rake datashift:spree:images input=C:\images\photos large dummy=true
10
- #
11
- # => rake datashift:spree:images input=C:\images\taxon_icons skip_if_no_assoc=true klass=Taxon
12
- #
13
- namespace :datashift do
14
-
15
- namespace :spree do
16
-
17
- #DEPRECATED FOTR THOR
18
- #desc "Populate the DB with images.\nDefault location db/image_seeds, or specify :input=<path> or dir under db/image_seeds with :folder"
19
- # :dummy => dummy run without actual saving to DB
20
- task :images, [:input, :folder, :dummy, :sku, :skip_if_no_assoc, :skip_if_loaded, :model] => :environment do |t, args|
21
-
22
- require 'image_loader'
23
-
24
- raise "USAGE: Please specify one of :input or :folder" if(args[:input] && args[:folder])
25
- puts "SKU not specified " if(args[:input] && args[:folder])
26
-
27
- if args[:input]
28
- @image_cache = args[:input]
29
- else
30
- @image_cache = File.join(Rails.root, "db", "image_seeds")
31
- @image_cache = File.join(@image_cache, args[:folder]) if(args[:folder])
32
- end
33
-
34
-
35
- attachment_klazz = SpreeHelper::get_spree_class('Product' )
36
- sku_klazz = SpreeHelper::get_spree_class('Variant' )
37
-
38
- image_loader = ImageLoader.new
39
-
40
-
41
- if(File.directory? @image_cache )
42
- puts "Loading images from #{@image_cache}"
43
-
44
- missing_records = []
45
- Dir.glob("#{@image_cache}/**/*.{jpg,png,gif}") do |image_name|
46
-
47
- base_name = File.basename(image_name, '.*')
48
-
49
- puts "Processing #{base_name} : #{File.exists?(image_name)}"
50
-
51
- record = nil
52
- if(args[:sku])
53
- sku = base_name.slice!(/\w+/)
54
- sku.strip!
55
- base_name.strip!
56
-
57
- puts "Looking fo SKU #{sku}"
58
- record = sku_klazz.find_by_sku(sku)
59
- if record
60
- record = record.product # SKU stored on Variant but we want it's master Product
61
- else
62
- puts "Looking for NAME [#{base_name}]"
63
- record = attachment_klazz.find_by_name(base_name)
64
- end
65
- else
66
- puts "Looking for #{attachment_klazz.name} with NAME [#{base_name}]"
67
- record = attachment_klazz.find_by_name(base_name)
68
- end
69
-
70
- if(record)
71
- puts "Found record for attachment : #{record.inspect}"
72
- exists = record.images.detect {|i| puts "COMPARE #{i.attachment_file_name} => #{image_name}"; i.attachment_file_name == image_name }
73
- puts "Found existing attachments [#{exists}]" unless(exists.nil?)
74
- if(args[:skip_if_loaded] && !exists.nil?)
75
- puts "Skipping - Image #{image_name} already loaded for #{attachment_klazz}"
76
- next
77
- end
78
- else
79
- missing_records << image_name
80
- end
81
-
82
- # Now do actual upload to DB unless we are doing a dummy run,
83
- # or the Image must have an associated record
84
- unless(args[:dummy] == 'true' || (args[:skip_if_no_assoc] && record.nil?))
85
- image_loader.reset()
86
- puts "Process Image"
87
- image_loader.process( image_name, record )
88
- end
89
-
90
- end
91
-
92
- unless missing_records.empty?
93
- FileUtils.mkdir_p('MissingRecords') unless File.directory?('MissingRecords')
94
-
95
- puts '\nMISSING Records Report>>'
96
- missing_records.each do |i|
97
- puts "Copy #{i} to MissingRecords folder"
98
- FileUtils.cp( i, 'MissingRecords') unless(args[:dummy] == 'true')
99
- end
100
- end
101
-
102
- else
103
- puts "ERROR: Supplied Path #{@image_cache} not accesible"
104
- exit(-1)
105
- end
106
- end
107
- end
108
- end