datashift 0.2.2 → 0.4.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.
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