datashift 0.10.1 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/Rakefile +6 -1
  2. data/VERSION +1 -1
  3. data/datashift.gemspec +13 -6
  4. data/lib/datashift.rb +2 -20
  5. data/lib/datashift/exceptions.rb +2 -0
  6. data/lib/datashift/method_detail.rb +15 -29
  7. data/lib/datashift/method_dictionary.rb +36 -21
  8. data/lib/datashift/method_mapper.rb +56 -16
  9. data/lib/datashift/populator.rb +23 -0
  10. data/lib/datashift/querying.rb +86 -0
  11. data/lib/generators/csv_generator.rb +1 -4
  12. data/lib/generators/excel_generator.rb +28 -11
  13. data/lib/generators/generator_base.rb +12 -0
  14. data/lib/loaders/csv_loader.rb +9 -3
  15. data/lib/loaders/excel_loader.rb +14 -6
  16. data/lib/loaders/loader_base.rb +38 -125
  17. data/lib/loaders/paperclip/attachment_loader.rb +130 -62
  18. data/lib/loaders/paperclip/datashift_paperclip.rb +46 -12
  19. data/lib/loaders/paperclip/image_loading.rb +25 -41
  20. data/lib/thor/generate.thor +16 -6
  21. data/lib/thor/paperclip.thor +25 -5
  22. data/spec/Gemfile +3 -2
  23. data/spec/MissingAttachmentRecords/DEMO_001_ror_bag.jpeg +0 -0
  24. data/spec/{fixtures/images/DEMO_002_Powerstation.jpg → MissingAttachmentRecords/DEMO_002_Powerstation.jpeg} +0 -0
  25. data/spec/MissingAttachmentRecords/DEMO_002_Powerstation.jpg +0 -0
  26. data/spec/MissingAttachmentRecords/DEMO_003_ror_mug.jpeg +0 -0
  27. data/spec/MissingAttachmentRecords/DEMO_004_ror_ringer.jpeg +0 -0
  28. data/spec/excel_generator_spec.rb +28 -0
  29. data/spec/excel_loader_spec.rb +12 -17
  30. data/spec/fixtures/config/database.yml +1 -1
  31. data/spec/fixtures/db/datashift_test_models_db.sqlite +0 -0
  32. data/spec/fixtures/db/migrate/20121009161700_add_digitals.rb +24 -0
  33. data/spec/fixtures/images/DEMO_002_Powerstation.jpeg +0 -0
  34. data/spec/fixtures/models/digital.rb +14 -0
  35. data/spec/fixtures/models/owner.rb +5 -3
  36. data/spec/fixtures/test_model_defs.rb +4 -62
  37. data/spec/loader_spec.rb +42 -50
  38. data/spec/method_dictionary_spec.rb +3 -10
  39. data/spec/method_mapper_spec.rb +79 -20
  40. data/spec/paperclip_loader_spec.rb +95 -0
  41. data/spec/spec_helper.rb +44 -8
  42. metadata +236 -224
  43. data/lib/helpers/rake_utils.rb +0 -42
  44. data/spec/fixtures/models/test_model_defs.rb +0 -67
data/Rakefile CHANGED
@@ -22,7 +22,12 @@ require 'rubygems'
22
22
 
23
23
  require 'rake'
24
24
 
25
- require 'lib/datashift'
25
+ lib = File.expand_path('../lib/', __FILE__)
26
+
27
+ $:.unshift '.'
28
+ $:.unshift lib unless $:.include?(lib)
29
+
30
+ require 'datashift'
26
31
 
27
32
  require 'jeweler'
28
33
  Jeweler::Tasks.new do |gem|
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.10.1
1
+ 0.10.2
data/datashift.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "datashift"
8
- s.version = "0.10.1"
8
+ s.version = "0.10.2"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Thomas Statter"]
12
- s.date = "2012-09-25"
12
+ s.date = "2012-10-21"
13
13
  s.description = "Comprehensive tools to import/export between Excel/CSV and ActiveRecord databases, Rails apps, and any Ruby project."
14
14
  s.email = "rubygems@autotelik.co.uk"
15
15
  s.extra_rdoc_files = [
@@ -46,6 +46,7 @@ Gem::Specification.new do |s|
46
46
  "lib/datashift/method_mapper.rb",
47
47
  "lib/datashift/model_mapper.rb",
48
48
  "lib/datashift/populator.rb",
49
+ "lib/datashift/querying.rb",
49
50
  "lib/exporters/csv_exporter.rb",
50
51
  "lib/exporters/excel_exporter.rb",
51
52
  "lib/exporters/exporter_base.rb",
@@ -54,7 +55,6 @@ Gem::Specification.new do |s|
54
55
  "lib/generators/generator_base.rb",
55
56
  "lib/guards.rb",
56
57
  "lib/helpers/core_ext/to_b.rb",
57
- "lib/helpers/rake_utils.rb",
58
58
  "lib/java/poi-3.7/._poi-3.7-20101029.jar5645100390082102460.tmp",
59
59
  "lib/java/poi-3.7/LICENSE",
60
60
  "lib/java/poi-3.7/NOTICE",
@@ -82,6 +82,11 @@ Gem::Specification.new do |s|
82
82
  "lib/thor/paperclip.thor",
83
83
  "lib/thor/tools.thor",
84
84
  "spec/Gemfile",
85
+ "spec/MissingAttachmentRecords/DEMO_001_ror_bag.jpeg",
86
+ "spec/MissingAttachmentRecords/DEMO_002_Powerstation.jpeg",
87
+ "spec/MissingAttachmentRecords/DEMO_002_Powerstation.jpg",
88
+ "spec/MissingAttachmentRecords/DEMO_003_ror_mug.jpeg",
89
+ "spec/MissingAttachmentRecords/DEMO_004_ror_ringer.jpeg",
85
90
  "spec/csv_exporter_spec.rb",
86
91
  "spec/csv_loader_spec.rb",
87
92
  "spec/datashift_spec.rb",
@@ -100,19 +105,20 @@ Gem::Specification.new do |s|
100
105
  "spec/fixtures/config/database.yml",
101
106
  "spec/fixtures/db/datashift_test_models_db.sqlite",
102
107
  "spec/fixtures/db/migrate/20110803201325_create_test_bed.rb",
108
+ "spec/fixtures/db/migrate/20121009161700_add_digitals.rb",
103
109
  "spec/fixtures/images/DEMO_001_ror_bag.jpeg",
104
- "spec/fixtures/images/DEMO_002_Powerstation.jpg",
110
+ "spec/fixtures/images/DEMO_002_Powerstation.jpeg",
105
111
  "spec/fixtures/images/DEMO_003_ror_mug.jpeg",
106
112
  "spec/fixtures/images/DEMO_004_ror_ringer.jpeg",
107
113
  "spec/fixtures/load_datashift.thor",
108
114
  "spec/fixtures/models/category.rb",
115
+ "spec/fixtures/models/digital.rb",
109
116
  "spec/fixtures/models/empty.rb",
110
117
  "spec/fixtures/models/loader_release.rb",
111
118
  "spec/fixtures/models/long_and_complex_table_linked_to_version.rb",
112
119
  "spec/fixtures/models/milestone.rb",
113
120
  "spec/fixtures/models/owner.rb",
114
121
  "spec/fixtures/models/project.rb",
115
- "spec/fixtures/models/test_model_defs.rb",
116
122
  "spec/fixtures/models/version.rb",
117
123
  "spec/fixtures/simple_export_spec.xls",
118
124
  "spec/fixtures/simple_template_spec.xls",
@@ -120,6 +126,7 @@ Gem::Specification.new do |s|
120
126
  "spec/loader_spec.rb",
121
127
  "spec/method_dictionary_spec.rb",
122
128
  "spec/method_mapper_spec.rb",
129
+ "spec/paperclip_loader_spec.rb",
123
130
  "spec/rails_sandbox/.gitignore",
124
131
  "spec/rails_sandbox/Gemfile",
125
132
  "spec/rails_sandbox/README.rdoc",
@@ -190,7 +197,7 @@ Gem::Specification.new do |s|
190
197
  s.homepage = "http://github.com/autotelik/datashift"
191
198
  s.licenses = ["MIT"]
192
199
  s.require_paths = ["lib"]
193
- s.rubygems_version = "1.8.15"
200
+ s.rubygems_version = "1.8.24"
194
201
  s.summary = "Shift data betwen Excel/CSV and any Ruby app"
195
202
 
196
203
  if s.respond_to? :specification_version then
data/lib/datashift.rb CHANGED
@@ -33,27 +33,9 @@
33
33
  # DataShift::load_commands
34
34
  #
35
35
  require 'rbconfig'
36
-
37
- module DataShift
38
-
39
- module Guards
40
-
41
- def self.jruby?
42
- return RUBY_PLATFORM == "java"
43
- end
44
- def self.mac?
45
- RbConfig::CONFIG['target_os'] =~ /darwin/i
46
- end
36
+ require 'guards'
47
37
 
48
- def self.linux?
49
- RbConfig::CONFIG['target_os'] =~ /linux/i
50
- end
51
-
52
- def self.windows?
53
- RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i
54
- end
55
-
56
- end
38
+ module DataShift
57
39
 
58
40
  if(Guards::jruby?)
59
41
  require 'java'
@@ -10,4 +10,6 @@ module DataShift
10
10
  class MissingHeadersError < StandardError; end
11
11
  class MissingMandatoryError < StandardError; end
12
12
 
13
+ class RecordNotFound < StandardError; end
14
+
13
15
  end
@@ -12,6 +12,7 @@
12
12
  #
13
13
  require 'to_b'
14
14
  require 'logging'
15
+ require 'populator'
15
16
  require 'set'
16
17
 
17
18
  module DataShift
@@ -20,6 +21,9 @@ module DataShift
20
21
 
21
22
  include DataShift::Logging
22
23
 
24
+ include DataShift::Populator
25
+ extend DataShift::Populator
26
+
23
27
  def self.supported_types_enum
24
28
  @type_enum ||= Set[:assignment, :belongs_to, :has_one, :has_many]
25
29
  @type_enum
@@ -35,13 +39,17 @@ module DataShift
35
39
  @@insistent_find_by_list ||= [:name, :title, :id]
36
40
 
37
41
  # Name is the raw, client supplied name
38
- attr_reader :name, :col_type, :current_value
42
+ attr_accessor :name
43
+ attr_accessor :column_index
44
+
45
+ # The rel col type from the DB
46
+ attr_reader :col_type, :current_value
39
47
 
40
48
  attr_reader :operator, :operator_type
41
49
 
42
50
  # TODO make it a list/primary keys
43
51
  attr_accessor :find_by_operator, :find_by_value
44
-
52
+
45
53
  # Store the raw (client supplied) name against the active record klass(model).
46
54
  # Operator is the associated method call on klass,
47
55
  # so client name maybe Price but true operator is price
@@ -68,6 +76,8 @@ module DataShift
68
76
  else
69
77
  @col_type = col_types[operator]
70
78
  end
79
+
80
+ @column_index = -1
71
81
  end
72
82
 
73
83
 
@@ -163,12 +173,6 @@ module DataShift
163
173
  "#{@name} => #{operator}"
164
174
  end
165
175
 
166
-
167
- def self.insistent_method_list
168
- @insistent_method_list ||= [:to_s, :to_i, :to_f, :to_b]
169
- @insistent_method_list
170
- end
171
-
172
176
  private
173
177
 
174
178
  # Attempt to find the associated object via id, name, title ....
@@ -188,7 +192,7 @@ module DataShift
188
192
  end
189
193
  rescue => e
190
194
  puts "ERROR: #{e.inspect}"
191
- if(x == MethodDetail::insistent_method_list.last)
195
+ if(x == Populator::insistent_method_list.last)
192
196
  raise "I'm sorry I have failed to assign [#{value}] to #{@assignment}" unless value.nil?
193
197
  end
194
198
  end
@@ -211,7 +215,7 @@ module DataShift
211
215
  end
212
216
  rescue => e
213
217
  puts "ERROR: #{e.inspect}"
214
- if(x == MethodDetail::insistent_method_list.last)
218
+ if(x == Populator::insistent_method_list.last)
215
219
  raise "I'm sorry I have failed to assign [#{value}] to #{operator}" unless value.nil?
216
220
  end
217
221
  end
@@ -220,25 +224,7 @@ module DataShift
220
224
  end
221
225
 
222
226
  def insistent_assignment( record, value )
223
- #puts "DEBUG: RECORD CLASS #{record.class}"
224
- op = operator + '='
225
-
226
- begin
227
- record.send(op, value)
228
- rescue => e
229
- MethodDetail::insistent_method_list.each do |f|
230
- begin
231
- record.send(op, value.send( f) )
232
- break
233
- rescue => e
234
- #puts "DEBUG: insistent_assignment: #{e.inspect}"
235
- if f == MethodDetail::insistent_method_list.last
236
- puts "I'm sorry I have failed to assign [#{value}] to #{operator}"
237
- raise "I'm sorry I have failed to assign [#{value}] to #{operator}" unless value.nil?
238
- end
239
- end
240
- end
241
- end
227
+ Populator::insistent_assignment( record, value, operator)
242
228
  end
243
229
 
244
230
  private
@@ -16,7 +16,6 @@ module DataShift
16
16
  def initialize
17
17
  end
18
18
 
19
-
20
19
  # Has the dictionary been populated for klass
21
20
  def self.for?(klass)
22
21
  return !(has_many[klass] || belongs_to[klass] || has_one[klass] || assignments[klass]).nil?
@@ -122,22 +121,12 @@ module DataShift
122
121
  method_details_mgrs[klass] = method_details_mgr
123
122
 
124
123
  end
125
-
126
- # Find the proper format of name, appropriate call + column type for a given name.
127
- # e.g Given users entry in spread sheet check for pluralization, missing underscores etc
128
- #
129
- # If not nil, returned method can be used directly in for example klass.new.send( call, .... )
130
- #
131
- def self.find_method_detail( klass, external_name )
132
-
133
- method_details_mgr = get_method_details_mgr( klass )
134
-
135
- # md_mgr.all_available_operators.each { |l| puts "DEBUG: Mapped Method : #{l.inspect}" }
136
-
124
+
125
+ # TODO - check out regexp to do this work better plus Inflections ??
126
+ # Want to be able to handle any of ["Count On hand", 'count_on_hand', "Count OnHand", "COUNT ONHand" etc]
127
+ def self.substitutions(external_name)
137
128
  name = external_name.to_s
138
-
139
- # TODO - check out regexp to do this work better plus Inflections ??
140
- # Want to be able to handle any of ["Count On hand", 'count_on_hand', "Count OnHand", "COUNT ONHand" etc]
129
+
141
130
  [
142
131
  name,
143
132
  name.tableize,
@@ -146,20 +135,46 @@ module DataShift
146
135
  name.gsub(/(\s+)/, '_').downcase,
147
136
  name.gsub(' ', ''),
148
137
  name.gsub(' ', '').downcase,
149
- name.gsub(' ', '_').underscore].each do |n|
150
-
151
- # Try each association type, returning first that contains matching operator with name n
138
+ name.gsub(' ', '_').underscore
139
+ ]
140
+ end
141
+
142
+ # Find the proper format of name, appropriate call + column type for a given name.
143
+ # e.g Given users entry in spread sheet check for pluralization, missing underscores etc
144
+ #
145
+ # If not nil, returned method can be used directly in for example klass.new.send( call, .... )
146
+ #
147
+ def self.find_method_detail( klass, external_name )
148
+
149
+ method_details_mgr = get_method_details_mgr( klass )
150
+
151
+ # md_mgr.all_available_operators.each { |l| puts "DEBUG: Mapped Method : #{l.inspect}" }
152
+ substitutions(external_name).each do |n|
152
153
 
154
+ # Try each association type, returning first that contains matching operator with name n
153
155
  MethodDetail::supported_types_enum.each do |t|
154
156
  method_detail = method_details_mgr.find(n, t)
155
157
  return method_detail.clone if(method_detail)
156
- end
157
-
158
+ end
158
159
  end
159
160
 
160
161
  nil
161
162
  end
163
+
164
+ # Assignments can contain things like delegated methods, this returns a matching
165
+ # method details only when a true database column
166
+ def self.find_method_detail_if_column( klass, external_name )
162
167
 
168
+ method_details_mgr = get_method_details_mgr( klass )
169
+
170
+ substitutions(external_name).each do |n|
171
+ method_detail = method_details_mgr.find(n, :assignment)
172
+ return method_detail if(method_detail && method_detail.col_type)
173
+ end
174
+
175
+ nil
176
+ end
177
+
163
178
  def self.clear
164
179
  belongs_to.clear
165
180
  has_many.clear
@@ -24,7 +24,6 @@ module DataShift
24
24
 
25
25
  include DataShift::Logging
26
26
 
27
- attr_accessor :header_row, :headers
28
27
  attr_accessor :method_details, :missing_methods
29
28
 
30
29
 
@@ -45,68 +44,109 @@ module DataShift
45
44
 
46
45
  def initialize
47
46
  @method_details = []
48
- @headers = []
49
47
  end
50
48
 
51
49
  # Build complete picture of the methods whose names listed in columns
52
50
  # Handles method names as defined by a user, from spreadsheets or file headers where the names
53
51
  # specified may not be exactly as required e.g handles capitalisation, white space, _ etc
54
- # Returns: Array of matching method_details
52
+ #
53
+ # The header can also contain the fields to use in lookups, separated with MethodMapper::column_delim
54
+ #
55
+ # product:name or project:title or user:email
56
+ #
57
+ # Returns: Array of matching method_details, including nils for non matched items
58
+ #
59
+ # N.B Columns that could not be mapped are left in the array as NIL
60
+ #
61
+ # This is to support clients that need to map via the index on @method_details
62
+ #
63
+ # Other callers can simply call compact on the results if the index not important.
64
+ #
65
+ # The Methoddetails instance will contain a pointer to the column index from which it was mapped.
66
+ #
67
+ #
68
+ # Options:
69
+ #
70
+ # [:force_inclusion] : List of columns that do not map to any operator but should be includeed in processing.
71
+ #
72
+ # This provides the opportunity for loaders to provide specific methods to handle these fields
73
+ # when no direct operator is available on the modle or it's associations
74
+ #
75
+ # [:include_all] : Include all headers in processing - takes precedence of :force_inclusion
55
76
  #
56
- def map_inbound_to_methods( klass, columns, options = {} )
77
+ def map_inbound_headers_to_methods( klass, columns, options = {} )
78
+
79
+ # If klass not in MethodDictionary yet, add to dictionary all possible operators on klass
80
+ # which can be used to map headers and populate an object of type klass
81
+ unless(MethodDictionary::for?(klass))
82
+ DataShift::MethodDictionary.find_operators(klass)
83
+
84
+ DataShift::MethodDictionary.build_method_details(klass)
85
+ end
57
86
 
58
87
  forced = [*options[:force_inclusion]].compact
59
88
  forced.collect! { |f| f.downcase }
60
89
 
61
90
  @method_details, @missing_methods = [], []
62
91
 
63
- columns.each do |name|
64
- if(name.nil? or name.empty?)
92
+ columns.each_with_index do |col_data, col_index|
93
+
94
+ raw_col_data = col_data.to_s
95
+
96
+ if(raw_col_data.nil? or raw_col_data.empty?)
65
97
  logger.warn("Column list contains empty or null columns")
66
98
  @method_details << nil
67
99
  next
68
100
  end
69
101
 
70
- operator, lookup = name.split(MethodMapper::column_delim)
102
+ raw_col_name, lookup = raw_col_data.split(MethodMapper::column_delim)
71
103
 
72
- md = MethodDictionary::find_method_detail( klass, operator )
104
+ md = MethodDictionary::find_method_detail( klass, raw_col_name )
73
105
 
74
106
  # TODO be nice if we could cheeck that the assoc on klass responds to the specified
75
107
  # lookup key now (nice n early)
76
108
  # active_record_helper = "find_by_#{lookup}"
77
- if(md.nil? && forced.include?(operator.downcase))
78
- md = MethodDictionary::add(klass, operator)
109
+ if(md.nil? && options[:include_all] || forced.include?(raw_col_name.downcase))
110
+ md = MethodDictionary::add(klass, raw_col_name)
79
111
  end
80
112
 
81
113
  if(md)
82
114
 
115
+ md.name = raw_col_name
116
+ md.column_index = col_index
117
+
83
118
  if(lookup)
84
119
  find_by, find_value = lookup.split(MethodMapper::column_delim)
85
120
  md.find_by_value = find_value
86
121
  md.find_by_operator = find_by # TODO and klass.x.respond_to?(active_record_helper))
87
122
  #puts "DEBUG: Method Detail #{md.name};#{md.operator} : find_by_operator #{md.find_by_operator}"
88
123
  end
89
-
90
- @method_details << md
91
124
  else
92
- @missing_methods << operator
125
+ # TODO populate unmapped with a real MethodDetail that is 'null' and create is_nil
126
+ @missing_methods << raw_col_name
93
127
  end
94
128
 
129
+ @method_details << md
130
+
95
131
  end
96
- #@method_details.compact! .. currently we may need to map via the index on @method_details so don't remove nils for now
132
+
97
133
  @method_details
98
134
  end
99
135
 
136
+
137
+ # TODO populate unmapped with a real MethodDetail that is 'null' and create is_nil
138
+ #
100
139
  # The raw client supplied names
101
140
  def method_names()
102
- @method_details.collect( &:name )
141
+ @method_details.compact.collect( &:name )
103
142
  end
104
143
 
105
144
  # The true operator names discovered from model
106
145
  def operator_names()
107
- @method_details.collect( &:operator )
146
+ @method_details.compact.collect( &:operator )
108
147
  end
109
148
 
149
+
110
150
  # Returns true if discovered methods contain every operator in mandatory_list
111
151
  def contains_mandatory?( mandatory_list )
112
152
  [ [*mandatory_list] - operator_names].flatten.empty?