datashift 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -35,7 +35,7 @@ module DataShift
35
35
  attr_reader :operator, :operator_type
36
36
 
37
37
  # TODO make it a list/primary keys
38
- attr_accessor :find_by_operator
38
+ attr_accessor :find_by_operator, :find_by_value
39
39
 
40
40
  # Store the raw (client supplied) name against the active record klass(model).
41
41
  # Operator is the associated method call on klass,
@@ -43,9 +43,10 @@ module DataShift
43
43
  #
44
44
  # col_types can typically be derived from klass.columns - set of ActiveRecord::ConnectionAdapters::Column
45
45
 
46
- def initialize(client_name, klass, operator, type, col_types = {}, find_by_operator = nil )
46
+ def initialize(client_name, klass, operator, type, col_types = {}, find_by_operator = nil, find_by_value = nil )
47
47
  @klass, @name = klass, client_name
48
48
  @find_by_operator = find_by_operator
49
+ @find_by_value = find_by_value
49
50
 
50
51
  if( MethodDetail::supported_types_enum.member?(type.to_sym) )
51
52
  @operator_type = type.to_sym
@@ -17,6 +17,11 @@ module DataShift
17
17
  end
18
18
 
19
19
 
20
+ # Has the dictionary been populated for klass
21
+ def self.for?(klass)
22
+ return !(has_many[klass] || belongs_to[klass] || has_one[klass] || assignments[klass]).nil?
23
+ end
24
+
20
25
  # Create simple picture of all the operator names for assignment available on an AR model,
21
26
  # grouped by type of association (includes belongs_to and has_many which provides both << and = )
22
27
  # Options:
@@ -43,7 +48,7 @@ module DataShift
43
48
  #puts "Belongs To Associations:", belongs_to[klass].inspect
44
49
 
45
50
  # Find the has_one associations which can be populated via Model.has_one_name = OtherArModelObject
46
- if( options[:reload] || self.has_one[klass].nil? )
51
+ if( options[:reload] || has_one[klass].nil? )
47
52
  has_one[klass] = klass.reflect_on_all_associations(:has_one).map { |i| i.name.to_s }
48
53
  end
49
54
 
@@ -145,7 +150,7 @@ module DataShift
145
150
 
146
151
  MethodDetail::supported_types_enum.each do |t|
147
152
  method_detail = method_details_mgr.find(n, t)
148
- return method_detail if(method_detail)
153
+ return method_detail.clone if(method_detail)
149
154
  end
150
155
 
151
156
  end
@@ -32,6 +32,9 @@ module DataShift
32
32
  # in the heading .. i.e Column header => 'BlogPosts:user_id'
33
33
  # ... association has many BlogPosts selected via find_by_user_id
34
34
  #
35
+ # in the heading .. i.e Column header => 'BlogPosts:user_name:John Smith'
36
+ # ... association has many BlogPosts selected via find_by_user_name("John Smith")
37
+ #
35
38
  def self.column_delim
36
39
  @column_delim ||= ':'
37
40
  @column_delim
@@ -60,11 +63,12 @@ module DataShift
60
63
  columns.each do |name|
61
64
  if(name.nil? or name.empty?)
62
65
  logger.warn("Column list contains empty or null columns")
66
+ @method_details << nil
63
67
  next
64
68
  end
65
69
 
66
70
  operator, lookup = name.split(MethodMapper::column_delim)
67
- #puts "DEBUG: Find Method Detail for #{x}"
71
+
68
72
  md = MethodDictionary::find_method_detail( klass, operator )
69
73
 
70
74
  # TODO be nice if we could cheeck that the assoc on klass responds to the specified
@@ -76,8 +80,13 @@ module DataShift
76
80
 
77
81
  if(md)
78
82
 
79
- md.find_by_operator = lookup if(lookup) # TODO and klass.x.respond_to?(active_record_helper))
80
-
83
+ if(lookup)
84
+ find_by, find_value = lookup.split(MethodMapper::column_delim)
85
+ md.find_by_value = find_value
86
+ md.find_by_operator = find_by # TODO and klass.x.respond_to?(active_record_helper))
87
+ #puts "DEBUG: Method Detail #{md.name};#{md.operator} : find_by_operator #{md.find_by_operator}"
88
+ end
89
+
81
90
  @method_details << md
82
91
  else
83
92
  @missing_methods << operator
@@ -6,19 +6,19 @@ class ModelMapper
6
6
  # e.g "Spree::Property" returns the Spree::Property class
7
7
  # Raises exception if no such class found
8
8
  def self.const_get_from_string(str)
9
- str.split('::').inject(Object) do |mod, class_name|
9
+ str.to_s.split('::').inject(Object) do |mod, class_name|
10
10
  mod.const_get(class_name)
11
11
  end
12
12
  end
13
-
14
-
13
+
14
+
15
15
  # Similar to const_get_from_string except this version
16
16
  # returns nil if no such class found
17
17
  # Support modules e.g "Spree::Property"
18
18
  #
19
19
  def self.class_from_string( str )
20
20
  begin
21
- ModelMapper::const_get_from_string(str) #Kernel.const_get(model)
21
+ ModelMapper::const_get_from_string(str.to_s) #Kernel.const_get(model)
22
22
  rescue NameError => e
23
23
  return nil
24
24
  end
@@ -27,7 +27,9 @@ module DataShift
27
27
  end
28
28
 
29
29
  # Create an Excel file template (header row) representing supplied Model
30
-
30
+ # Options:
31
+ # * <tt>:autosize</tt> - Autosize all the columns
32
+ #
31
33
  def generate(klass, options = {})
32
34
 
33
35
  prepare(klass, options)
@@ -36,18 +38,25 @@ module DataShift
36
38
 
37
39
  logger.info("ExcelGenerator saving generated template #{@filename}")
38
40
 
41
+ @excel.autosize if(options[:autosize])
42
+
39
43
  @excel.save( @filename )
40
44
  end
41
45
 
42
46
 
43
- # Create an Excel file from list of ActiveRecord objects
44
- # To remove type(s) of associations specify option :
45
- # :exclude => [type(s)]
47
+ # Create an Excel file template (header row) representing supplied Model
48
+ # and it's associations
49
+ #
50
+ # Options:
51
+ # * <tt>:autosize</tt> - Autosize all the columns
52
+ #
53
+ # * <tt>:exclude</tt> - Associations to exclude.
54
+ # You can specify a hash of {association_type => [array of association names] }
55
+ # to exclude from the template.
46
56
  #
47
- # Possible values are given by MethodDetail::supported_types_enum
57
+ # Possible association_type values are given by MethodDetail::supported_types_enum
48
58
  # ... [:assignment, :belongs_to, :has_one, :has_many]
49
59
  #
50
- # Options
51
60
  def generate_with_associations(klass, options = {})
52
61
 
53
62
  prepare(klass, options)
@@ -69,6 +78,8 @@ module DataShift
69
78
  end
70
79
 
71
80
  @excel.set_headers( headers )
81
+
82
+ @excel.autosize if(options[:autosize])
72
83
 
73
84
  @excel.save( filename() )
74
85
  end
@@ -66,7 +66,7 @@ module DataShift
66
66
  # For example if model has an attribute 'price' will map columns called Price, price, PRICE etc to this attribute
67
67
  map_headers_to_operators( @headers, options )
68
68
 
69
- logger.info "Excel Loader prcoessing #{@excel.num_rows} rows"
69
+ logger.info "Excel Loader processing #{@excel.num_rows} rows"
70
70
  load_object_class.transaction do
71
71
  @loaded_objects = []
72
72
 
@@ -124,8 +124,9 @@ module DataShift
124
124
  end
125
125
  end
126
126
  puts "Excel loading stage complete - #{loaded_objects.size} rows added."
127
+ puts "There were NO failures." if failed_objects.empty?
127
128
 
128
- puts "WARNING : #{failed_objects.size} rows contained errors and #{failed_objects.size} records NOT created." unless failed_objects.empty?
129
+ puts "WARNING : Check logs : #{failed_objects.size} rows contained errors and #{failed_objects.size} records NOT created." unless failed_objects.empty?
129
130
  end
130
131
 
131
132
  def value_at(row, column)
@@ -85,21 +85,33 @@ module DataShift
85
85
  def self.set_multi_assoc_delim(x) @multi_assoc_delim = x; end
86
86
 
87
87
 
88
+ # Setup loading
89
+ #
90
+ # Options to drive building the method dictionary for a class, enabling headers to be mapped to operators on that class.
91
+ #
88
92
  # Options
89
- # :instance_methods => true
93
+ # :load [default = true] : Load the method dictionary for object_class
94
+ #
95
+ # :reload : Force load of the method dictionary for object_class even if already loaded
96
+ # :instance_methods : Include setter type instance methods for assignment as well as AR columns
90
97
 
98
+
91
99
  def initialize(object_class, object = nil, options = {})
92
100
  @load_object_class = object_class
93
101
 
94
102
  # Gather names of all possible 'setter' methods on AR class (instance variables and associations)
95
- DataShift::MethodDictionary.find_operators( @load_object_class, :reload => true, :instance_methods => options[:instance_methods] )
96
-
97
- # Create dictionary of data on all possible 'setter' methods which can be used to
98
- # populate or integrate an object of type @load_object_class
99
- DataShift::MethodDictionary.build_method_details(@load_object_class)
103
+ unless(MethodDictionary::for?(object_class) && options[:reload] == false)
104
+ #puts "Building Method Dictionary for class #{object_class}"
105
+ DataShift::MethodDictionary.find_operators( @load_object_class, :reload => options[:reload], :instance_methods => options[:instance_methods] )
106
+
107
+ # Create dictionary of data on all possible 'setter' methods which can be used to
108
+ # populate or integrate an object of type @load_object_class
109
+ DataShift::MethodDictionary.build_method_details(@load_object_class)
110
+ end unless(options[:load] == false)
100
111
 
101
112
  @method_mapper = DataShift::MethodMapper.new
102
- @options = options.clone
113
+ @options = options.dup # clone can cause issues like 'can't modify frozen hash'
114
+
103
115
  @verbose = @options[:verbose]
104
116
  @headers = []
105
117
 
@@ -127,8 +139,8 @@ module DataShift
127
139
  # mandatory : List of columns that must be present in headers
128
140
  #
129
141
  # force_inclusion : List of columns that do not map to any operator but should be includeed in processing.
130
- # This provides the opportunity for loaders to provide specific methods to handle these fields
131
- # when no direct operator is available on the modle or it's associations
142
+ # This provides the opportunity for loaders to provide specific methods to handle these fields
143
+ # when no direct operator is available on the model or it's associations
132
144
  #
133
145
  def perform_load( file_name, options = {} )
134
146
 
@@ -179,7 +191,7 @@ module DataShift
179
191
  end
180
192
 
181
193
  unless(@method_mapper.missing_methods.empty?)
182
- puts "WARNING: Following column headings could not be mapped : #{@method_mapper.missing_methods.inspect}"
194
+ puts "WARNING: These headings couldn't be mapped to class #{load_object_class} : #{@method_mapper.missing_methods.inspect}"
183
195
  raise MappingDefinitionError, "Missing mappings for columns : #{@method_mapper.missing_methods.join(",")}" if(strict)
184
196
  end
185
197
 
@@ -214,18 +226,20 @@ module DataShift
214
226
  end
215
227
  end
216
228
 
217
- def get_record_by(klazz, field, value)
218
- x = (@options[:sku_prefix]) ? "#{@options[:sku_prefix]}#{value}" : value
219
-
229
+
230
+ # Find a record for model klazz, looking up on field for x
231
+ # Responds to global Options :
232
+ # :case_sensitive : Default is a case insensitive lookup.
233
+ # :use_like : Attempts a lookup using ike and x% ratehr than equality
234
+
235
+ def get_record_by(klazz, field, x)
236
+
220
237
  begin
221
238
  if(@options[:case_sensitive])
222
- puts "Search case sensitive for [#{x}] on #{field}" if(@verbose)
223
239
  return klazz.send("find_by_#{field}", x)
224
240
  elsif(@options[:use_like])
225
- puts "Term : #{klazz}.where(\"#{field} LIKE '#{x}%'\")" if(@verbose)
226
241
  return klazz.where("#{field} like ?", "#{x}%").first
227
242
  else
228
- puts "Term : #{klazz}.where(\"lower(#{field}) = '#{x.downcase}'\")" if(@verbose)
229
243
  return klazz.where("lower(#{field}) = ?", x.downcase).first
230
244
  end
231
245
  rescue => e
@@ -238,8 +252,10 @@ module DataShift
238
252
 
239
253
  # Default values and over rides can be provided in YAML config file.
240
254
  #
241
- # Any Configuration under key 'LoaderBase' is merged into this classes
242
- # existing options - taking precedence.
255
+ # Any Config under key 'LoaderBase' is merged over existing options - taking precedence.
256
+ #
257
+ # Any Config under a key equal to the full name of the Loader class (e.g DataShift::SpreeHelper::ImageLoader)
258
+ # is merged over existing options - taking precedence.
243
259
  #
244
260
  # Format :
245
261
  #
@@ -297,6 +313,10 @@ module DataShift
297
313
  if(data['LoaderBase'])
298
314
  @options.merge!(data['LoaderBase'])
299
315
  end
316
+
317
+ if(data[self.class.name])
318
+ @options.merge!(data[self.class.name])
319
+ end
300
320
 
301
321
  logger.info("Loader Options : #{@options.inspect}")
302
322
  end
@@ -326,18 +346,31 @@ module DataShift
326
346
  @current_value
327
347
  end
328
348
 
329
- # return the find_by operator and the values to find
330
- def get_find_operator_and_rest( column_data)
331
-
332
- find_operator, col_values = "",nil
333
-
349
+ # Return the find_by operator and the rest of the (row,columns) data
350
+ # price:0.99
351
+ #
352
+ # Column headings can already contain the operator so possible that row only contains
353
+ # 0.99
354
+ # We leave it to caller to manage any other aspects or problems in 'rest'
355
+ #
356
+ def get_find_operator_and_rest(inbound_data)
357
+
358
+ operator, rest = inbound_data.split(LoaderBase::name_value_delim)
359
+
360
+ #puts "DEBUG inbound_data: #{inbound_data} => #{operator} , #{rest}"
361
+
362
+ # Find by operator embedded in row takes precedence over operator in column heading
334
363
  if(@current_method_detail.find_by_operator)
335
- find_operator, col_values = @current_method_detail.find_by_operator, column_data
336
- else
337
- find_operator, col_values = column_data.split(LoaderBase::name_value_delim)
364
+ # row contains 0.99 so rest is effectively operator, and operator is in method details
365
+ if(rest.nil?)
366
+ rest = operator
367
+ operator = @current_method_detail.find_by_operator
368
+ end
338
369
  end
339
370
 
340
- return find_operator, col_values
371
+ #puts "DEBUG: get_find_operator_and_rest: #{operator} => #{rest}"
372
+
373
+ return operator, rest
341
374
  end
342
375
 
343
376
  # Process a value string from a column.
@@ -377,6 +410,8 @@ module DataShift
377
410
  raise "Cannot perform DB find by #{find_operator}. Expected format key:value" unless(find_operator && col_values)
378
411
 
379
412
  find_by_values = col_values.split(LoaderBase::multi_value_delim)
413
+
414
+ find_by_values << @current_method_detail.find_by_value if(@current_method_detail.find_by_value)
380
415
 
381
416
  if(find_by_values.size > 1)
382
417
 
@@ -0,0 +1,75 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2012
2
+ # Author :: Tom Statter
3
+ # Date :: June 2012
4
+ # License:: MIT. Free, Open Source.
5
+ #
6
+ # => Provides facilities for bulk uploading/exporting attachments provided by PaperClip
7
+ # gem
8
+ require 'loader_base'
9
+
10
+ module DataShift
11
+
12
+ module ImageLoading
13
+
14
+ include DataShift::Logging
15
+
16
+ def self.get_files(path, options = {})
17
+ glob = (options['recursive'] || options[:recursive]) ? "**/*.{jpg,png,gif}" : "*.{jpg,png,gif}"
18
+
19
+ Dir.glob("#{path}/#{glob}")
20
+ end
21
+
22
+ def get_file( attachment_path )
23
+
24
+ unless File.exists?(attachment_path) && File.readable?(attachment_path)
25
+ logger.error("Cannot process Image from #{Dir.pwd}: Invalid Path #{attachment_path}")
26
+ raise "Cannot process Image : Invalid Path #{attachment_path}"
27
+ end
28
+
29
+ file = begin
30
+ File.new(attachment_path, "rb")
31
+ rescue => e
32
+ puts e.inspect
33
+ raise "ERROR : Failed to read image #{attachment_path}"
34
+ end
35
+
36
+ file
37
+ end
38
+
39
+ # Note the paperclip attachment model defines the storage path via something like :
40
+ # => :path => ":rails_root/public/blah/blahs/:id/:style/:basename.:extension"
41
+ # Options
42
+ # has_attached_file_name : Paperclip attachment name defined with macro 'has_attached_file :name' e.g has_attached_file :avatar
43
+ #
44
+ def create_image(klass, attachment_path, viewable_record = nil, options = {})
45
+
46
+ has_attached_file = options[:has_attached_file_name] || :attachment
47
+
48
+ alt = if(options[:alt])
49
+ options[:alt]
50
+ else
51
+ (viewable_record and viewable_record.respond_to? :name) ? viewable_record.name : ""
52
+ end
53
+
54
+ position = (viewable_record and viewable_record.respond_to?(:images)) ? viewable_record.images.length : 0
55
+
56
+ file = get_file(attachment_path)
57
+
58
+ begin
59
+
60
+ image = klass.new(
61
+ {has_attached_file.to_sym => file, :viewable => viewable_record, :alt => alt, :position => position},
62
+ :without_protection => true
63
+ )
64
+
65
+ #image.attachment.reprocess! not sure this is required anymore
66
+
67
+ puts image.save ? "Success: Created Image: #{image.id} : #{image.attachment_file_name}" : "ERROR : Problem saving to DB Image: #{image.inspect}"
68
+ rescue => e
69
+ puts "PaperClip error - Problem creating an Image from : #{attachment_path}"
70
+ puts e.inspect, e.backtrace
71
+ end
72
+ end
73
+ end
74
+
75
+ end
@@ -4,58 +4,24 @@
4
4
  # License:: MIT. Free, Open Source.
5
5
  #
6
6
  require 'loader_base'
7
+ require 'paperclip/image_loader'
7
8
 
8
9
  module DataShift
9
10
 
10
11
 
11
- module ImageLoading
12
+ module DataShift::SpreeImageLoading
12
13
 
13
14
  include DataShift::Logging
15
+ include DataShift::ImageLoading
14
16
 
15
- def get_file( attachment_path )
16
-
17
- unless File.exists?(attachment_path) && File.readable?(attachment_path)
18
- logger.error("Cannot process Image from #{Dir.pwd}: Invalid Path #{attachment_path}")
19
- raise "Cannot process Image : Invalid Path #{attachment_path}"
20
- end
21
-
22
- file = begin
23
- File.new(attachment_path, "rb")
24
- rescue => e
25
- puts e.inspect
26
- raise "ERROR : Failed to read image #{attachment_path}"
27
- end
28
-
29
- file
30
- end
31
-
32
17
  # Note the Spree Image model sets default storage path to
33
18
  # => :path => ":rails_root/public/assets/products/:id/:style/:basename.:extension"
34
19
 
35
20
  def create_image(klass, attachment_path, viewable_record = nil, options = {})
36
-
37
- alt = if(options[:alt])
38
- options[:alt]
39
- else
40
- (viewable_record and viewable_record.respond_to? :name) ? viewable_record.name : ""
41
- end
42
-
43
- position = (viewable_record and viewable_record.respond_to?(:images)) ? viewable_record.images.length : 0
44
-
45
- file = get_file(attachment_path)
46
-
47
- if(SpreeHelper::version.to_f > 1 && viewable_record.is_a?(Spree::Product) )
48
-
49
- image = klass.new( :attachment => file, :alt => alt, :position => position)
50
21
 
51
- # mass assignment not allows for this field
52
- image.viewable = viewable_record.master
53
- else
54
- image = klass.new( :attachment => file,:viewable => viewable_record, :alt => alt, :position => position)
55
- end
56
- #image.attachment.reprocess!
57
-
58
- puts image.save ? "Success: Created Image: #{image.inspect}" : "ERROR : Problem saving to DB Image: #{image.inspect}"
22
+ viewable = (SpreeHelper::version.to_f > 1 && viewable_record.is_a?(Spree::Product) ) ? viewable_record.master : viewable_record
23
+
24
+ super(klass, attachment_path, viewable, options)
59
25
  end
60
26
  end
61
27
 
@@ -64,12 +30,15 @@ module DataShift
64
30
  # TODO - extract this out of SpreeHelper to create a general paperclip loader
65
31
  class ImageLoader < LoaderBase
66
32
 
67
- include DataShift::ImageLoading
33
+ include DataShift::SpreeImageLoading
68
34
  include DataShift::CsvLoading
69
35
  include DataShift::ExcelLoading
70
36
 
71
37
  def initialize(image = nil, options = {})
72
- super( SpreeHelper::get_spree_class('Image'), image, options )
38
+
39
+ opts = options.merge(:load => false) # Don't need operators and no table Spree::Image
40
+
41
+ super( SpreeHelper::get_spree_class('Image'), image, opts )
73
42
 
74
43
  if(SpreeHelper::version.to_f > 1.0 )
75
44
  @attachment_klazz = DataShift::SpreeHelper::get_spree_class('Variant' )