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