datashift 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -39,6 +39,12 @@ module DataShift
39
39
  logger.debug "PRODUCT #{@load_object.inspect} MASTER: #{@load_object.master.inspect}"
40
40
  end
41
41
 
42
+ def perform_load( file_name, options = {} )
43
+ # In >= 1.1.0 Image moved to master Variant from Product so no association called Images on Product anymore
44
+ options[:force_inclusion] = options[:force_inclusion] ? ['images'] : [*options[:force_inclusion]] << 'images'
45
+
46
+ super(file_name, options)
47
+ end
42
48
 
43
49
  # Over ride base class process with some Spree::Product specifics
44
50
  #
@@ -120,68 +126,100 @@ module DataShift
120
126
  # Special case for OptionTypes as it's two stage process
121
127
  # First add the possible option_types to Product, then we are able
122
128
  # to define Variants on those options values.
129
+ # To defiene a Variant :
130
+ # 1) define at least one OptionType on Product, for example Size
131
+ # 2) Provide a value for at least one of these OptionType
132
+ # 3) A composite Variant can be created by supplyiung a vlaue for more than one OptionType
133
+ # fro example Colour : Red and Size Medium
134
+ # Supported Syntax :
135
+ # '|' seperates Variants
136
+ # ',' list of option values
137
+ # Examples :
138
+ # mime_type:jpeg;print_type:black_white|mime_type:jpeg|mime_type:png, PDF;print_type:colour
123
139
  #
124
140
  def add_options
125
141
 
126
142
  # TODO smart column ordering to ensure always valid by time we get to associations
127
143
  save_if_new
128
144
 
129
- option_types = get_each_assoc#current_value.split( LoaderBase::multi_assoc_delim )
130
-
131
- option_types.each do |ostr|
132
- oname, value_str = ostr.split(LoaderBase::name_value_delim)
145
+ # example : mime_type:jpeg;print_type:black_white|mime_type:jpeg|mime_type:png, PDF;print_type:colour
146
+
147
+ variants = get_each_assoc#current_value.split( LoaderBase::multi_assoc_delim )
133
148
 
134
- option_type = @@option_type_klass.find_by_name(oname)
149
+ # 1) mime_type:jpeg;print_type:black_white 2) mime_type:jpeg 3) mime_type:png, PDF;print_type:colour
135
150
 
136
- unless option_type
137
- option_type = @@option_type_klass.create( :name => oname, :presentation => oname.humanize)
138
- # TODO - dynamic creation should be an option
151
+ variants.each do |per_variant|
152
+
153
+ option_types = per_variant.split(';') # [mime_type:jpeg, print_type:black_white]
154
+
155
+ optiontype_vlist_map = {}
156
+
157
+ option_types.each do |ostr|
158
+
159
+ oname, value_str = ostr.split(LoaderBase::name_value_delim)
160
+
161
+ option_type = @@option_type_klass.find_by_name(oname)
139
162
 
140
163
  unless option_type
141
- puts "WARNING: OptionType #{oname} NOT found - Not set Product"
142
- next
164
+ option_type = @@option_type_klass.create( :name => oname, :presentation => oname.humanize)
165
+ # TODO - dynamic creation should be an option
166
+
167
+ unless option_type
168
+ puts "WARNING: OptionType #{oname} NOT found and could not create - Not set Product"
169
+ next
170
+ end
171
+ puts "Created missing OptionType #{option_type.inspect}"
143
172
  end
144
- puts "Created missing OptionType #{option_type.inspect}"
145
- end
146
173
 
147
- @load_object.option_types << option_type unless @load_object.option_types.include?(option_type)
174
+ # OptionTypes must be specified first on Product to enable Variants to be created
175
+ # TODO - is include? very inefficient ??
176
+ @load_object.option_types << option_type unless @load_object.option_types.include?(option_type)
148
177
 
149
- # Can be simply list of OptionTypes, some or all without values
150
- next unless(value_str)
178
+ # Can be simply list of OptionTypes, some or all without values
179
+ next unless(value_str)
151
180
 
152
- # Now get the value(s) for the option e.g red,blue,green for OptType 'colour'
153
- ovalues = value_str.split(',')
181
+ optiontype_vlist_map[option_type] = []
182
+
183
+ # Now get the value(s) for the option e.g red,blue,green for OptType 'colour'
184
+ optiontype_vlist_map[option_type] = value_str.split(',')
185
+ end
186
+
187
+ next if(optiontype_vlist_map.empty?) # only option types specified - no values
154
188
 
189
+ # Now create set of Variants, some of which maybe composites
190
+ # Find the longest set of OVs to use as base for combining with the rest
191
+ sorted_map = optiontype_vlist_map.sort_by { |k,v| v.size }.reverse
192
+
193
+ # ovalues = 'pdf','jpeg','png'
194
+ option_type, ovalues = sorted_map.shift
155
195
  # TODO .. benchmarking to find most efficient way to create these but ensure Product.variants list
156
196
  # populated .. currently need to call reload to ensure this (seems reqd for Spree 1/Rails 3, wasn't required b4
157
- ovalues.each_with_index do |ovname, i|
158
- ovname.strip!
159
- ov = @@option_value_klass.find_or_create_by_name_and_option_type_id(ovname, option_type.id)
160
- if ov
161
- begin
162
- # This one line seems to works for 1.1.0 - 3.2 but not 1.0.0 - 3.1 ??
163
- if(SpreeHelper::version.to_f >= 1.1)
164
- variant = @load_object.variants.create( :sku => "#{@load_object.sku}_#{i}", :price => @load_object.price)
165
- else
166
- variant = @@variant_klass.create( :product => @load_object, :sku => "#{@load_object.sku}_#{i}", :price => @load_object.price, :available_on => @load_object.available_on)
167
- #if(variant.valid?)
168
- # variant.save
169
- # @load_object.variants << variant
170
-
171
- #else
172
- #puts "WARNING: For Option #{ovname} - Variant creation failed #{variant.errors.inspect}"
173
- end
174
- variant.option_values << ov
175
- rescue => e
176
- puts "Failed to create a Variant for Product #{@load_object.name}"
177
- puts e.inspect
178
- #puts e.backtrace
179
- end
197
+ ovalues.each do |ovname|
180
198
 
181
- logger.debug "Created New Variant: #{variant.inspect}"
199
+ ov_list = []
200
+
201
+ ov = @@option_value_klass.find_or_create_by_name_and_option_type_id(ovname.strip, option_type.id)
202
+
203
+ ov_list << ov if ov
182
204
 
183
- else
184
- puts "WARNING: Option #{ovname} NOT FOUND - No Variant created"
205
+ sorted_map.each do |ot, ovlist| ovlist.each do |for_composite|
206
+ ov = @@option_value_klass.find_or_create_by_name_and_option_type_id(for_composite.strip, ot.id)
207
+
208
+ ov_list << ov if(ov)
209
+ end
210
+ end
211
+
212
+ unless(ov_list.empty?)
213
+ i = @load_object.variants.size + 1
214
+
215
+ # This one line seems to works for 1.1.0 - 3.2 but not 1.0.0 - 3.1 ??
216
+ if(SpreeHelper::version.to_f >= 1.1)
217
+ variant = @load_object.variants.create( :sku => "#{@load_object.sku}_#{i}", :price => @load_object.price)
218
+ else
219
+ variant = @@variant_klass.create( :product => @load_object, :sku => "#{@load_object.sku}_#{i}", :price => @load_object.price, :available_on => @load_object.available_on)
220
+ end
221
+
222
+ variant.option_values << ov_list if(variant)
185
223
  end
186
224
  end
187
225
 
@@ -190,7 +228,7 @@ module DataShift
190
228
  #puts "DEBUG Load Object now has Variants : #{@load_object.variants.inspect}"
191
229
  end
192
230
 
193
- end
231
+ end # each Variant
194
232
 
195
233
  # Special case for Images
196
234
  # A list of paths to Images with a optional 'alt' value - supplied in form :
@@ -203,10 +241,13 @@ module DataShift
203
241
  images = get_each_assoc#current_value.split(LoaderBase::multi_assoc_delim)
204
242
 
205
243
  images.each do |image|
206
-
244
+ puts "Add Image #{image}"
207
245
  img_path, alt_text = image.split(LoaderBase::name_value_delim)
208
246
 
209
- image = create_image(@@image_klass, img_path, @load_object, :alt => alt_text)
247
+ # moved from Prod to Variant in spree 1.x.x
248
+ attachment = (SpreeHelper::version.to_f > 1) ? @load_object.master : @load_object
249
+
250
+ image = create_image(@@image_klass, img_path, attachment, :alt => alt_text)
210
251
  logger.debug("Product assigned an Image : #{image.inspect}")
211
252
  end
212
253
 
@@ -226,12 +267,13 @@ module DataShift
226
267
  property_list = get_each_assoc#current_value.split(LoaderBase::multi_assoc_delim)
227
268
 
228
269
  property_list.each do |pstr|
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)
270
+
271
+ # Special case, we know we lookup on name so operator is effectively the name to lookup
272
+ find_by_name, find_by_value = get_find_operator_and_rest( pstr )
273
+
274
+ raise "Cannot find Property via #{find_by_name} (with value #{find_by_value})" unless(find_by_name)
233
275
 
234
- property = @@property_klass.find_by_name(find_by_name)
276
+ property = @@property_klass.find_by_name(find_by_name)
235
277
 
236
278
  unless property
237
279
  property = @@property_klass.create( :name => find_by_name, :presentation => find_by_name.humanize)
@@ -244,8 +286,8 @@ module DataShift
244
286
  x = @@product_property_klass.new( :value => find_by_value )
245
287
  x.property = property
246
288
  x.save
247
- logger.info "Created New ProductProperty #{x.inspect}"
248
289
  @load_object.product_properties << x
290
+ logger.info "Created New ProductProperty #{x.inspect}"
249
291
  else
250
292
  @load_object.product_properties << @@product_property_klass.create( :property => property, :value => find_by_values)
251
293
  end
@@ -13,7 +13,8 @@
13
13
  #
14
14
  # Cmd Line:
15
15
  #
16
- # => bundle exec thor datashift:generate:excel -m <active record class> -r <output_template.xls> -a
16
+ # => bundle exec thor list datashift
17
+ # bundle exec thor help datashift:generate:excel
17
18
  #
18
19
  require 'datashift'
19
20
 
@@ -49,9 +50,11 @@ module Datashift
49
50
  klass = ModelMapper::class_from_string(model) #Kernel.const_get(model)
50
51
  rescue NameError => e
51
52
  puts e
52
- raise "ERROR: No such Model [#{model}] found - check valid model supplied via -model <Class>"
53
+ raise Thor::Error.new("ERROR: No such Model [#{model}] found - check valid model supplied")
53
54
  end
54
55
 
56
+ raise Thor::Error.new("ERROR: No such Model [#{model}] found - check valid model supplied") unless(klass)
57
+
55
58
  begin
56
59
  gen = DataShift::ExcelGenerator.new(result)
57
60
 
@@ -8,6 +8,10 @@
8
8
  # bundle exec thor datashift:spreeboot:cleanup
9
9
  #
10
10
  # Note, not DataShift, case sensitive, create namespace for command line : datashift
11
+
12
+ require File.expand_path('config/environment.rb')
13
+
14
+
11
15
  module Datashift
12
16
 
13
17
  class Spreeboot < Thor
@@ -22,31 +26,33 @@ module Datashift
22
26
 
23
27
  require File.expand_path('config/environment.rb')
24
28
 
29
+ ActiveRecord::Base.connection.execute("TRUNCATE spree_products_taxons")
30
+
25
31
  cleanup = %w{ Image OptionType OptionValue
26
32
  Product Property ProductGroup ProductProperty ProductOptionType
27
- Variant Taxonomy Taxon Zone
33
+ Variant Taxonomy Taxon
28
34
  }
29
35
 
30
36
  cleanup.each do |k|
31
37
  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}"
38
+ if(klass)
39
+ puts "Clearing model #{klass}"
40
+ klass.delete_all
41
+ else
42
+ puts "WARNING - Could not find AR model for class name #{k}"
43
+ end
37
44
  end
38
-
39
- end
40
45
 
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
46
+ image_bank = 'public/spree/products'
45
47
 
46
- FileUtils::rm_rf('MissingRecords') if(File.exists?('MissingRecords') )
48
+ if(File.exists?(image_bank) )
49
+ puts "Removing old Product assets from '#{image_bank}'"
50
+ FileUtils::rm_rf(image_bank)
51
+ end
47
52
 
48
- end
53
+ FileUtils::rm_rf('MissingRecords') if(File.exists?('MissingRecords') )
54
+
55
+ end
49
56
 
50
- end
51
-
57
+ end
52
58
  end
@@ -21,7 +21,7 @@ module Datashift
21
21
  desc "products", "Populate Spree Product/Variant data from .xls (Excel) or CSV file"
22
22
 
23
23
  method_option :input, :aliases => '-i', :required => true, :desc => "The import file (.xls or .csv)"
24
- method_option :sku_prefix, :aliases => '-s', :desc => "Prefix to add to each SKU in import file"
24
+ method_option :sku_prefix, :aliases => '-s', :desc => "Prefix to add to each SKU before saving Product"
25
25
  method_option :verbose, :aliases => '-v', :type => :boolean, :desc => "Verbose logging"
26
26
  method_option :config, :aliases => '-c', :type => :string, :desc => "Configuration file containg defaults or over rides in YAML"
27
27
 
@@ -53,7 +53,12 @@ module Datashift
53
53
 
54
54
  puts "DataShift::Product starting upload from file: #{input}"
55
55
 
56
- loader.perform_load(input, :mandatory => ['sku', 'name', 'price'] )
56
+ options = {:mandatory => ['sku', 'name', 'price']}
57
+
58
+ # In >= 1.1.0 Image moved to master Variant from Product
59
+ options[:force_inclusion] = ['images'] if(DataShift::SpreeHelper::version.to_f > 1 )
60
+
61
+ loader.perform_load(input, options)
57
62
  end
58
63
 
59
64
 
@@ -79,32 +84,36 @@ module Datashift
79
84
 
80
85
 
81
86
  #
82
- # => rake datashift:spree:images input=vendor/extensions/site/fixtures/images
87
+ # => thor datashift:spree:images input=vendor/extensions/site/fixtures/images
83
88
  # => rake datashift:spree:images input=C:\images\photos large dummy=true
84
89
  #
85
90
  # => rake datashift:spree:images input=C:\images\taxon_icons skip_if_no_assoc=true klass=Taxon
86
91
  #
87
- desc "images", "Populate the DB with images from a directory where image names map to Product Sku/Name"
92
+ desc "images", "Populate the DB with images from a directory\nwhere image names contain somewhere the Product Sku/Name"
88
93
 
89
94
  # :dummy => dummy run without actual saving to DB
90
- method_option :input, :aliases => '-i', :required => true, :desc => "The import file (.xls or .csv)"
91
-
92
- method_option :process_when_no_assoc, :aliases => '-f', :type => :boolean, :desc => "Process image even if no Product found - force loading"
95
+ method_option :input, :aliases => '-i', :required => true, :desc => "The input path containing images (.jpg, .gif, .png)"
93
96
 
97
+ method_option :recursive, :aliases => '-r', :type => :boolean, :desc => "Scan sub directories of input for images"
98
+
94
99
  method_option :sku, :aliases => '-s', :desc => "Lookup Product based on image name starting with sku"
95
- method_option :sku_prefix, :aliases => '-p', :desc => "Prefix to add to each SKU in import file"
100
+ method_option :sku_prefix, :aliases => '-p', :desc => "Prefix to add to each SKU in import file before attempting lookup"
96
101
  method_option :dummy, :aliases => '-d', :type => :boolean, :desc => "Dummy run, do not actually save Image or Product"
102
+
103
+ method_option :process_when_no_assoc, :aliases => '-f', :type => :boolean, :desc => "Process image even if no Product found - force loading"
104
+ method_option :skip_when_assoc, :aliases => '-x', :type => :boolean, :desc => "DO not process image if Product already has image"
105
+
97
106
  method_option :verbose, :aliases => '-v', :type => :boolean, :desc => "Verbose logging"
98
107
  method_option :config, :aliases => '-c', :type => :string, :desc => "Configuration file for Image Loader in YAML"
99
108
  method_option :split_file_name_on, :type => :string, :desc => "delimiter to progressivley split filename for Prod lookup", :default => '_'
100
109
  method_option :case_sensitive, :type => :boolean, :desc => "Use case sensitive where clause to find Product"
101
- method_option :use_like, :type => :boolean, :desc => "Use LIKE 'string%' instead of = 'string' in where clauses"
110
+ method_option :use_like, :type => :boolean, :desc => "Use sku/name LIKE 'string%' instead of sku/name = 'string' in where clauses to find Product"
102
111
 
103
112
  def images()
104
113
 
105
114
  require File.expand_path('config/environment.rb')
106
115
 
107
- require 'image_loader'
116
+ require 'spree/image_loader'
108
117
 
109
118
  @verbose = options[:verbose]
110
119
 
@@ -116,59 +125,53 @@ module Datashift
116
125
  attachment_klazz = DataShift::SpreeHelper::get_spree_class('Product' )
117
126
  attachment_field = 'name'
118
127
 
119
- if(options[:sku])
128
+ if(options[:sku] || SpreeHelper::version.to_f > 1)
120
129
  attachment_klazz = DataShift::SpreeHelper::get_spree_class('Variant' )
121
130
  attachment_field = 'sku'
122
131
  end
123
-
124
- # TODO generalise for any paperclip project, for now just Spree
125
- #begin
126
- # attachment_klazz = Kernel.const_get(args[:model]) if(args[:model])
127
- # rescue NameError
128
- # raise "Could not find contant for model #{args[:model]}"
129
- #end
130
132
 
131
- image_loader = DataShift::SpreeHelper::ImageLoader.new(nil, options.dup)
132
-
133
- @loader_config = {}
134
-
133
+ image_loader = DataShift::SpreeHelper::ImageLoader.new(nil, options)
134
+
135
135
  if(options[:config])
136
136
  raise "Bad Config - Cannot find specified file #{options[:config]}" unless File.exists?(options[:config])
137
137
 
138
138
  image_loader.configure_from( options[:config] )
139
-
140
- @loader_config = YAML::load( File.open(options[:config]) )
141
-
142
- @loader_config = @loader_config['ImageLoader']
143
139
  end
140
+
141
+ loader_config = image_loader.options
142
+
143
+ puts "CONFIG: #{loader_config.inspect}"
144
+ puts "OPTIONS #{options.inspect}"
144
145
 
145
- puts "CONFIG: #{@loader_config.inspect}"
146
-
147
- @image_cache = options[:input]
146
+ @image_path = options[:input]
148
147
 
149
- unless(File.directory? @image_cache )
150
- puts "ERROR: Supplied Path #{@image_cache} not accesible"
148
+ unless(File.exists?(@image_path))
149
+ puts "ERROR: Supplied Path [#{@image_path}] not accesible"
151
150
  exit(-1)
152
151
  end
153
152
 
154
- logger.info "Loading Spree images from #{@image_cache}"
153
+ logger.info "Loading Spree images from #{@image_path}"
155
154
 
156
155
  missing_records = []
157
156
 
158
- # unless record # try splitting up filename in various ways looking for the SKU
159
- split_on = @loader_config[:split_file_name_on] || options[:split_file_name_on]
160
-
161
- Dir.glob("#{@image_cache}/**/*.{jpg,png,gif}") do |image_name|
157
+ # try splitting up filename in various ways looking for the SKU
158
+ split_on = loader_config['split_file_name_on'] || options[:split_file_name_on]
159
+
160
+ puts "Will scan image names splitting on delimiter : #{split_on}"
161
+
162
+ image_cache = DataShift::ImageLoading::get_files(@image_path, options)
163
+
164
+ image_cache.each do |image_name|
162
165
 
166
+ image_base_name = File.basename(image_name)
167
+
163
168
  base_name = File.basename(image_name, '.*')
164
169
  base_name.strip!
165
170
 
166
171
  logger.info "Processing image file #{base_name} : #{File.exists?(image_name)}"
167
172
 
168
173
  record = nil
169
-
170
- puts "Search for product for image file [#{base_name}]" if(@verbose)
171
-
174
+
172
175
  record = image_loader.get_record_by(attachment_klazz, attachment_field, base_name)
173
176
 
174
177
  # try seperate portions of the filename, front -> back
@@ -189,11 +192,17 @@ module Datashift
189
192
  if(record)
190
193
  logger.info "Found record for attachment : #{record.inspect}"
191
194
 
192
- if(options[:skip_if_loaded])
193
- exists = record.images.detect {|i| i.attachment_file_name == image_name }
195
+ if(options[:skip_when_assoc])
194
196
 
195
- logger.info "Skipping - Image #{image_name} already loaded for #{attachment_klazz}"
196
- next if(exists)
197
+ paper_clip_name = image_base_name.gsub(Paperclip::Attachment::default_options[:restricted_characters], '_')
198
+
199
+ exists = record.images.detect {|i| puts "Check #{paper_clip_name} matches #{i.attachment_file_name}"; i.attachment_file_name == paper_clip_name }
200
+ if(exists)
201
+ rid = record.respond_to?(:name) ? record.name : record.id
202
+ puts "Skipping Image #{image_name} already loaded for #{rid}"
203
+ logger.info "Skipping - Image #{image_name} already loaded for #{attachment_klazz}"
204
+ next
205
+ end
197
206
  end
198
207
  else
199
208
  missing_records << image_name
@@ -204,9 +213,10 @@ module Datashift
204
213
  # Check if Image must have an associated record
205
214
  if(record || (record.nil? && options[:process_when_no_assoc]))
206
215
  image_loader.reset()
207
- puts "Adding Image #{image_name} to Product #{record.name}" if(@verbose)
216
+
208
217
  logger.info("Adding Image #{image_name} to Product #{record.name}")
209
218
  image_loader.create_image(image_klazz, image_name, record)
219
+ puts "Added Image #{File.basename(image_name)} to Product #{record.sku} : #{record.name}" if(@verbose)
210
220
  end
211
221
 
212
222
  end
@@ -214,11 +224,14 @@ module Datashift
214
224
  unless missing_records.empty?
215
225
  FileUtils.mkdir_p('MissingRecords') unless File.directory?('MissingRecords')
216
226
 
217
- puts 'MISSING Records Report>>'
227
+ puts "WARNING : #{missing_records.size} of #{image_cache.size} images could not be attached to a Product"
228
+ puts 'Copying all images with MISSING Records to ./MissingRecords >>'
218
229
  missing_records.each do |i|
219
230
  puts "Copy #{i} to MissingRecords folder"
220
231
  FileUtils.cp( i, 'MissingRecords') unless(options[:dummy] == 'true')
221
232
  end
233
+ else
234
+ puts "All images (#{image_cache.size}) were succesfully attached to a Product"
222
235
  end
223
236
 
224
237
  puts "Dummy Run Complete- if happy run without -d" if(options[:dummy])