datashift 0.7.0 → 0.8.0

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.
@@ -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])