datashift 0.2.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/README.markdown +15 -3
  2. data/VERSION +1 -1
  3. data/datashift.gemspec +11 -3
  4. data/lib/applications/jruby/jexcel_file.rb +10 -3
  5. data/lib/datashift.rb +25 -62
  6. data/lib/datashift/exceptions.rb +1 -0
  7. data/lib/datashift/logging.rb +58 -0
  8. data/lib/datashift/method_detail.rb +6 -45
  9. data/lib/datashift/method_details_manager.rb +54 -0
  10. data/lib/datashift/method_dictionary.rb +6 -1
  11. data/lib/datashift/method_mapper.rb +12 -5
  12. data/lib/datashift/populator.rb +46 -0
  13. data/lib/exporters/excel_exporter.rb +1 -1
  14. data/lib/generators/excel_generator.rb +48 -44
  15. data/lib/helpers/spree_helper.rb +14 -3
  16. data/lib/loaders/csv_loader.rb +9 -6
  17. data/lib/loaders/excel_loader.rb +5 -1
  18. data/lib/loaders/loader_base.rb +28 -14
  19. data/lib/loaders/spree/image_loader.rb +36 -34
  20. data/lib/loaders/spree/product_loader.rb +17 -7
  21. data/lib/thor/generate_excel.thor +35 -15
  22. data/lib/thor/import_excel.thor +84 -0
  23. data/lib/thor/spree/bootstrap_cleanup.thor +33 -0
  24. data/lib/thor/spree/products_images.thor +171 -0
  25. data/spec/datashift_spec.rb +19 -0
  26. data/spec/excel_exporter_spec.rb +3 -3
  27. data/spec/fixtures/datashift_Spree_db.sqlite +0 -0
  28. data/spec/fixtures/datashift_test_models_db.sqlite +0 -0
  29. data/spec/fixtures/spree/SpreeProductsDefaults.yml +15 -0
  30. data/spec/fixtures/spree/SpreeProductsMandatoryOnly.xls +0 -0
  31. data/spec/fixtures/spree/SpreeProductsWithImages.xls +0 -0
  32. data/spec/spec_helper.rb +2 -2
  33. data/spec/spree_generator_spec.rb +14 -0
  34. data/spec/spree_images_loader_spec.rb +73 -0
  35. data/spec/spree_loader_spec.rb +53 -19
  36. data/tasks/spree/image_load.rake +18 -13
  37. metadata +11 -3
  38. data/tasks/spree/product_loader.rake +0 -44
@@ -11,6 +11,8 @@ module DataShift
11
11
 
12
12
  class MethodDictionary
13
13
 
14
+ include DataShift::Logging
15
+
14
16
  def initialize
15
17
  end
16
18
 
@@ -22,12 +24,15 @@ module DataShift
22
24
  # :instance_methods => if true include instance method type assignment operators as well as model's pure columns
23
25
  #
24
26
  def self.find_operators(klass, options = {} )
27
+
28
+ raise "Cannot find operators supplied klass nil #{klass}" if(klass.nil?)
25
29
 
26
30
  # Find the has_many associations which can be populated via <<
27
31
  if( options[:reload] || has_many[klass].nil? )
28
32
  has_many[klass] = klass.reflect_on_all_associations(:has_many).map { |i| i.name.to_s }
29
33
  klass.reflect_on_all_associations(:has_and_belongs_to_many).inject(has_many[klass]) { |x,i| x << i.name.to_s }
30
34
  end
35
+
31
36
  # puts "DEBUG: Has Many Associations:", has_many[klass].inspect
32
37
 
33
38
  # Find the belongs_to associations which can be populated via Model.belongs_to_name = OtherArModelObject
@@ -48,7 +53,7 @@ module DataShift
48
53
  # Note, not all reflections return method names in same style so we convert all to
49
54
  # the raw form i.e without the '=' for consistency
50
55
  if( options[:reload] || assignments[klass].nil? )
51
-
56
+
52
57
  assignments[klass] = klass.column_names
53
58
 
54
59
  if(options[:instance_methods] == true)
@@ -22,6 +22,8 @@ module DataShift
22
22
 
23
23
  class MethodMapper
24
24
 
25
+ include DataShift::Logging
26
+
25
27
  attr_accessor :header_row, :headers
26
28
  attr_accessor :method_details, :missing_methods
27
29
 
@@ -43,16 +45,21 @@ module DataShift
43
45
  @headers = []
44
46
  end
45
47
 
46
- # Build complete picture of the methods whose names listed in method_list
47
- # Handles method names as defined by a user or in file headers where names may
48
- # not be exactly as required e.g handles capitalisation, white space, _ etc
48
+ # Build complete picture of the methods whose names listed in columns
49
+ # Handles method names as defined by a user, from spreadsheets or file headers where the names
50
+ # specified may not be exactly as required e.g handles capitalisation, white space, _ etc
49
51
  # Returns: Array of matching method_details
50
52
  #
51
- def map_inbound_to_methods( klass, method_list )
53
+ def map_inbound_to_methods( klass, columns )
52
54
 
53
55
  @method_details, @missing_methods = [], []
54
56
 
55
- method_list.each do |name|
57
+ columns.each do |name|
58
+ if(name.nil? or name.empty?)
59
+ logger.warn("Column list contains empty or null columns")
60
+ next
61
+ end
62
+
56
63
  x, lookup = name.split(MethodMapper::column_delim)
57
64
  md = MethodDictionary::find_method_detail( klass, x )
58
65
 
@@ -0,0 +1,46 @@
1
+ # Copyright:: (c) Autotelik Media Ltd 2012
2
+ # Author :: Tom Statter
3
+ # Date :: March 2012
4
+ # License:: MIT
5
+ #
6
+ # Details:: This modules provides individual population methods on an AR model.
7
+ #
8
+ # Enables users to assign values to AR object, without knowing much about that receiving object.
9
+ #
10
+ require 'to_b'
11
+
12
+ module DataShift
13
+
14
+ module Populator
15
+
16
+ def self.insistent_method_list
17
+ @insistent_method_list ||= [:to_s, :to_i, :to_f, :to_b]
18
+ @insistent_method_list
19
+ end
20
+
21
+ def assignment( operator, record, value )
22
+ #puts "DEBUG: RECORD CLASS #{record.class}"
23
+ op = operator + '=' unless(operator.include?('='))
24
+
25
+ begin
26
+ record.send(op, value)
27
+ rescue => e
28
+ Populator::insistent_method_list.each do |f|
29
+ begin
30
+ record.send(op, value.send( f) )
31
+ break
32
+ rescue => e
33
+ #puts "DEBUG: insistent_assignment: #{e.inspect}"
34
+ if f == Populator::insistent_method_list.last
35
+ puts "I'm sorry I have failed to assign [#{value}] to #{operator}"
36
+ raise "I'm sorry I have failed to assign [#{value}] to #{operator}" unless value.nil?
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+
44
+ end
45
+
46
+ end
@@ -108,7 +108,7 @@ module DataShift
108
108
 
109
109
  def initialize(filename)
110
110
  @filename = filename
111
- raise DataShift::BadRuby, "Apologies but Datashift Excel facilities currently need JRuby. Please switch to, or install JRuby"
111
+ raise DataShift::BadRuby, "Apologies but DataShift Excel facilities currently need JRuby. Please switch to, or install JRuby"
112
112
  end
113
113
  end
114
114
  end # jruby
@@ -18,7 +18,9 @@ module DataShift
18
18
 
19
19
  class ExcelGenerator < GeneratorBase
20
20
 
21
- attr_accessor :filename
21
+ include DataShift::Logging
22
+
23
+ attr_accessor :excel, :filename
22
24
 
23
25
  def initialize(filename)
24
26
  @filename = filename
@@ -27,70 +29,72 @@ module DataShift
27
29
  # Create an Excel file template (header row) representing supplied Model
28
30
 
29
31
  def generate(klass, options = {})
30
- MethodDictionary.find_operators( klass )
31
-
32
- @filename = options[:filename] if options[:filename]
33
-
34
- excel = JExcelFile.new()
35
-
36
- if(options[:sheet_name] )
37
- excel.create_sheet( options[:sheet_name] )
38
- else
39
- excel.create_sheet( klass.name )
40
- end
32
+
33
+ prepare(klass, options)
41
34
 
42
- raise "Failed to create Excel WorkSheet for #{klass.name}" unless excel.sheet
35
+ @excel.set_headers(MethodDictionary.assignments[klass])
43
36
 
44
- excel.set_headers(MethodDictionary.assignments[klass])
45
-
46
- excel.save( @filename )
37
+ logger.info("ExcelGenerator saving generated template #{@filename}")
38
+
39
+ @excel.save( @filename )
47
40
  end
48
41
 
49
42
 
50
43
  # Create an Excel file from list of ActiveRecord objects
51
- # Specify which associations to export via :with or :exclude
52
- # Possible values are : [:assignment, :belongs_to, :has_one, :has_many]
44
+ # To remove type(s) of associations specify option :
45
+ # :exclude => [type(s)]
46
+ #
47
+ # Possible values are given by MethodDetail::supported_types_enum
48
+ # ... [:assignment, :belongs_to, :has_one, :has_many]
53
49
  #
50
+ # Options
54
51
  def generate_with_associations(klass, options = {})
55
52
 
56
- excel = JExcelFile.new()
57
-
58
- if(options[:sheet_name] )
59
- excel.create_sheet( options[:sheet_name] )
60
- else
61
- excel.create_sheet( klass.name )
62
- end
63
-
64
- MethodDictionary.find_operators( klass )
53
+ prepare(klass, options)
65
54
 
66
55
  MethodDictionary.build_method_details( klass )
67
56
 
68
- work_list = options[:with] || MethodDetail::supported_types_enum
69
-
57
+ work_list = MethodDetail::supported_types_enum.to_a
58
+ work_list -= options[:exclude].to_a
59
+
70
60
  headers = []
71
- puts "work_list : [#{work_list.inspect}]"
72
61
 
73
62
  details_mgr = MethodDictionary.method_details_mgrs[klass]
74
63
 
75
64
  work_list.each do |op_type|
76
65
  list_for_class_and_op = details_mgr.get_list(op_type)
77
-
66
+
78
67
  next if(list_for_class_and_op.nil? || list_for_class_and_op.empty?)
79
- #if(work_list.include?(md.operator_type))
80
- #each do |mdtype|
81
- #end
82
- #if(MethodDictionary.respond_to?("#{mdtype}_for") )
83
- # method_details = MethodDictionary.send("#{mdtype}_for", klass)
84
-
85
- list_for_class_and_op.each {|md| headers << "#{md.operator}" }
86
- #else
87
- # puts "ERROR : Unknown option in :with [#{mdtype}]"
88
-
68
+ list_for_class_and_op.each {|md| headers << "#{md.operator}" }
89
69
  end
90
70
 
91
- excel.set_headers( headers )
71
+ @excel.set_headers( headers )
92
72
 
93
- excel.save( filename() )
73
+ @excel.save( filename() )
74
+ end
75
+
76
+ private
77
+
78
+ def prepare(klass, options = {})
79
+ @filename = options[:filename] if options[:filename]
80
+
81
+ logger.info("ExcelGenerator creating template with associations for class #{klass}")
82
+
83
+ @excel = JExcelFile.new()
84
+
85
+ if(options[:sheet_name] )
86
+ @excel.create_sheet( options[:sheet_name] )
87
+ else
88
+ @excel.create_sheet( klass.name )
89
+ end
90
+
91
+ unless @excel.sheet
92
+ logger.error("Excel failed to create WorkSheet for #{klass.name}")
93
+
94
+ raise "Failed to create Excel WorkSheet for #{klass.name}"
95
+ end
96
+
97
+ MethodDictionary.find_operators( klass )
94
98
  end
95
99
  end # ExcelGenerator
96
100
 
@@ -99,7 +103,7 @@ module DataShift
99
103
 
100
104
  def initialize(filename)
101
105
  @filename = filename
102
- raise DataShift::BadRuby, "Apologies but Datashift Excel facilities currently need JRuby. Please switch to, or install JRuby"
106
+ raise DataShift::BadRuby, "Apologies but DataShift Excel facilities currently need JRuby. Please switch to, or install JRuby"
103
107
  end
104
108
  end
105
109
  end # jruby
@@ -25,8 +25,6 @@
25
25
  # as the database is auto generated
26
26
  # =>
27
27
 
28
-
29
-
30
28
  module DataShift
31
29
 
32
30
  module SpreeHelper
@@ -72,10 +70,15 @@ module DataShift
72
70
  end
73
71
 
74
72
 
75
- # Datahift isi usually included and tasks pulled in by a parent/host application.
73
+ # Datashift is usually included and tasks pulled in by a parent/host application.
76
74
  # So here we are hacking our way around the fact that datashift is not a Rails/Spree app/engine
77
75
  # so that we can ** run our specs ** directly in datashift library
78
76
  # i.e without ever having to install datashift in a host application
77
+ #
78
+ # NOTES:
79
+ # => Will chdir into the sandbox to load environment as need to mimic being at root of a rails project
80
+ # chdir back after environment loaded
81
+
79
82
  def self.boot( database_env )
80
83
 
81
84
  if( ! is_namespace_version )
@@ -96,10 +99,18 @@ module DataShift
96
99
 
97
100
  require 'rails/all'
98
101
 
102
+ store_path = Dir.pwd
103
+
99
104
  Dir.chdir( File.expand_path('../../../sandbox', __FILE__) )
100
105
 
106
+ rails_root = File.expand_path('../../../sandbox', __FILE__)
107
+
108
+ $:.unshift rails_root
109
+
101
110
  require 'config/environment.rb'
102
111
 
112
+ Dir.chdir( store_path )
113
+
103
114
  @dslog.info "Booted Spree using post 1.0.0 version"
104
115
  end
105
116
  end
@@ -14,8 +14,10 @@ module DataShift
14
14
 
15
15
  module CsvLoading
16
16
 
17
+ include DataShift::Logging
18
+
17
19
  def perform_csv_load(file_name, options = {})
18
-
20
+
19
21
  require "csv"
20
22
 
21
23
  # TODO - can we abstract out what a 'parsed file' is - so a common object can represent excel,csv etc
@@ -23,14 +25,12 @@ module DataShift
23
25
 
24
26
  @parsed_file = CSV.read(file_name)
25
27
 
26
-
27
- @method_mapper = DataShift::MethodMapper.new
28
-
29
28
  @mandatory = options[:mandatory] || []
30
29
 
31
30
  # Create a method_mapper which maps list of headers into suitable calls on the Active Record class
31
+ # For example if model has an attribute 'price' will map columns called Price, price, PRICE etc to this attribute
32
32
  map_headers_to_operators( @parsed_file.shift, options[:strict] , @mandatory )
33
-
33
+
34
34
  unless(@method_mapper.missing_methods.empty?)
35
35
  puts "WARNING: Following column headings could not be mapped : #{@method_mapper.missing_methods.inspect}"
36
36
  raise MappingDefinitionError, "ERROR: Missing mappings for #{@method_mapper.missing_methods.size} column headings"
@@ -45,6 +45,9 @@ module DataShift
45
45
  @loaded_objects = []
46
46
 
47
47
  @parsed_file.each do |row|
48
+
49
+ # First assign any default values for columns not included in parsed_file
50
+ process_missing_columns_with_defaults
48
51
 
49
52
  # TODO - Smart sorting of column processing order ....
50
53
  # Does not currently ensure mandatory columns (for valid?) processed first but model needs saving
@@ -67,7 +70,7 @@ module DataShift
67
70
  # TODO - handle when it's not valid ?
68
71
  # Process rest and dump out an exception list of Products ??
69
72
 
70
- puts "SAVING ROW #{row} : #{load_object.inspect}" #if options[:verbose]
73
+ logger.info "Saving csv row #{row} to table object : #{load_object.inspect}" #if options[:verbose]
71
74
 
72
75
  save
73
76
 
@@ -80,6 +80,10 @@ module DataShift
80
80
  break if @excel.sheet.getRow(row).nil?
81
81
 
82
82
  contains_data = false
83
+
84
+ # First assign any default values for columns not included in parsed_file
85
+ process_missing_columns_with_defaults
86
+
83
87
 
84
88
  # TODO - Smart sorting of column processing order ....
85
89
  # Does not currently ensure mandatory columns (for valid?) processed first but model needs saving
@@ -87,7 +91,7 @@ module DataShift
87
91
 
88
92
  # as part of this we also attempt to save early, for example before assigning to
89
93
  # has_and_belongs_to associations which require the load_object has an id for the join table
90
-
94
+
91
95
  # Iterate over the columns method_mapper found in Excel,
92
96
  # pulling data out of associated column
93
97
  @method_mapper.method_details.each_with_index do |method_detail, col|
@@ -16,8 +16,8 @@ module DataShift
16
16
 
17
17
  class LoaderBase
18
18
 
19
-
20
19
  include DataShift::Logging
20
+ include DataShift::Populator
21
21
 
22
22
  attr_reader :headers
23
23
 
@@ -131,8 +131,13 @@ module DataShift
131
131
  def map_headers_to_operators( headers, strict, mandatory = [])
132
132
  @headers = headers
133
133
 
134
- method_details = @method_mapper.map_inbound_to_methods( load_object_class, @headers )
135
-
134
+ begin
135
+ method_details = @method_mapper.map_inbound_to_methods( load_object_class, @headers )
136
+ rescue => e
137
+ logger.error("Failed to map header row to set of database operators : #{e.inspect}")
138
+ raise MappingDefinitionError, "Failed to map header row to set of database operators"
139
+ end
140
+
136
141
  unless(@method_mapper.missing_methods.empty?)
137
142
  puts "WARNING: Following column headings could not be mapped : #{@method_mapper.missing_methods.inspect}"
138
143
  raise MappingDefinitionError, "Missing mappings for columns : #{@method_mapper.missing_methods.join(",")}" if(strict)
@@ -145,6 +150,15 @@ module DataShift
145
150
  end
146
151
 
147
152
 
153
+ # Process any defaults user has specified, for those columns that are not included in
154
+ # the incoming import format
155
+ def process_missing_columns_with_defaults()
156
+ inbound_ops = @method_mapper.operator_names
157
+ @default_values.each do |dn, dv|
158
+ assignment(dn, @load_object, dv) unless(inbound_ops.include?(dn))
159
+ end
160
+ end
161
+
148
162
  # Core API - Given a single free text column name from a file, search method mapper for
149
163
  # associated operator on base object class.
150
164
  #
@@ -176,25 +190,25 @@ module DataShift
176
190
  # IDEAS .....
177
191
  #
178
192
  #unless(@default_data_objects[load_object_class])
179
- #
180
- # @default_data_objects[load_object_class] = load_object_class.new
193
+ #
194
+ # @default_data_objects[load_object_class] = load_object_class.new
181
195
 
182
196
  # default_data_object = @default_data_objects[load_object_class]
183
197
 
184
198
 
185
- # default_data_object.instance_eval do
186
- # def datashift_defaults=(hash)
187
- # @datashift_defaults = hash
188
- # end
189
- # def datashift_defaults
190
- # @datashift_defaults
191
- # end
192
- #end unless load_object_class.respond_to?(:datashift_defaults)
199
+ # default_data_object.instance_eval do
200
+ # def datashift_defaults=(hash)
201
+ # @datashift_defaults = hash
202
+ # end
203
+ # def datashift_defaults
204
+ # @datashift_defaults
205
+ # end
206
+ #end unless load_object_class.respond_to?(:datashift_defaults)
193
207
  #end
194
208
 
195
209
  #puts load_object_class.new.to_yaml
196
210
 
197
- puts data.inspect
211
+ logger.info("Read Datashift loading config: #{data.inspect}")
198
212
 
199
213
  if(data[load_object_class.name])
200
214