datashift 0.10.1 → 0.10.2

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 (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?