datashift 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/README.markdown +91 -55
  3. data/VERSION +1 -1
  4. data/datashift.gemspec +8 -23
  5. data/lib/applications/jexcel_file.rb +1 -2
  6. data/lib/datashift.rb +34 -15
  7. data/lib/datashift/column_packer.rb +98 -34
  8. data/lib/datashift/data_transforms.rb +83 -0
  9. data/lib/datashift/delimiters.rb +58 -10
  10. data/lib/datashift/excel_base.rb +123 -0
  11. data/lib/datashift/exceptions.rb +45 -7
  12. data/lib/datashift/load_object.rb +25 -0
  13. data/lib/datashift/mapping_service.rb +91 -0
  14. data/lib/datashift/method_detail.rb +40 -62
  15. data/lib/datashift/method_details_manager.rb +18 -2
  16. data/lib/datashift/method_dictionary.rb +27 -10
  17. data/lib/datashift/method_mapper.rb +49 -41
  18. data/lib/datashift/model_mapper.rb +42 -22
  19. data/lib/datashift/populator.rb +258 -143
  20. data/lib/datashift/thor_base.rb +38 -0
  21. data/lib/exporters/csv_exporter.rb +57 -145
  22. data/lib/exporters/excel_exporter.rb +73 -60
  23. data/lib/generators/csv_generator.rb +65 -5
  24. data/lib/generators/generator_base.rb +69 -3
  25. data/lib/generators/mapping_generator.rb +112 -0
  26. data/lib/helpers/core_ext/csv_file.rb +33 -0
  27. data/lib/loaders/csv_loader.rb +41 -39
  28. data/lib/loaders/excel_loader.rb +130 -116
  29. data/lib/loaders/loader_base.rb +190 -146
  30. data/lib/loaders/paperclip/attachment_loader.rb +4 -4
  31. data/lib/loaders/paperclip/datashift_paperclip.rb +5 -3
  32. data/lib/loaders/paperclip/image_loading.rb +9 -7
  33. data/lib/loaders/reporter.rb +17 -8
  34. data/lib/thor/export.thor +12 -13
  35. data/lib/thor/generate.thor +1 -9
  36. data/lib/thor/import.thor +13 -24
  37. data/lib/thor/mapping.thor +65 -0
  38. data/spec/Gemfile +13 -11
  39. data/spec/Gemfile.lock +98 -93
  40. data/spec/csv_exporter_spec.rb +104 -99
  41. data/spec/csv_generator_spec.rb +159 -0
  42. data/spec/csv_loader_spec.rb +197 -16
  43. data/spec/datashift_spec.rb +9 -0
  44. data/spec/excel_exporter_spec.rb +149 -58
  45. data/spec/excel_generator_spec.rb +35 -44
  46. data/spec/excel_loader_spec.rb +196 -178
  47. data/spec/excel_spec.rb +8 -5
  48. data/spec/loader_base_spec.rb +47 -7
  49. data/spec/mapping_spec.rb +117 -0
  50. data/spec/method_dictionary_spec.rb +24 -11
  51. data/spec/method_mapper_spec.rb +5 -7
  52. data/spec/model_mapper_spec.rb +41 -0
  53. data/spec/paperclip_loader_spec.rb +3 -6
  54. data/spec/populator_spec.rb +48 -14
  55. data/spec/spec_helper.rb +85 -73
  56. data/spec/thor_spec.rb +40 -5
  57. metadata +93 -86
  58. data/lib/applications/excel_base.rb +0 -63
@@ -3,16 +3,13 @@
3
3
  # Date :: Aug 2010
4
4
  # License:: MIT
5
5
  #
6
- # Details:: This class provides info and access to the individual population methods
6
+ # Details:: This class provides info and access to the details of individual population methods
7
7
  # on an AR model. Populated by, and coupled with MethodMapper,
8
8
  # which does the model interrogation work and stores sets of MethodDetails.
9
9
  #
10
10
  # Enables 'loaders' to iterate over the MethodMapper results set,
11
11
  # and assign values to AR object, without knowing anything about that receiving object.
12
12
  #
13
- require 'to_b'
14
- require 'logging'
15
- require 'populator'
16
13
  require 'set'
17
14
 
18
15
  module DataShift
@@ -32,21 +29,35 @@ module DataShift
32
29
  end
33
30
 
34
31
 
35
- # Name is the raw, client supplied name
32
+ def self.is_association_type? ( type )
33
+ association_types_enum.member?( type )
34
+ end
35
+
36
+
37
+ # Klass is the class of the 'parent' object i.e with the associations,
38
+ # For example Product which may have operator orders
39
+ attr_accessor :klass
40
+
41
+ # Name is the raw, client supplied name e.g Orders
36
42
  attr_accessor :name
37
43
  attr_accessor :column_index
38
44
 
39
45
  # The rel col type from the DB
40
- attr_reader :col_type, :current_value
46
+ attr_reader :col_type
41
47
 
48
+ # The :operator that can be called to assign e.g orders or Products.new.orders << Order.new
49
+ #
50
+ # The type of operator e.g :assignment, :belongs_to, :has_one, :has_many etc
42
51
  attr_reader :operator, :operator_type
43
52
 
44
53
  # TODO make it a list/primary keys
54
+ # Additional helpers for where clauses
45
55
  attr_accessor :find_by_operator, :find_by_value
46
56
 
47
57
  # Store the raw (client supplied) name against the active record klass(model).
48
58
  # Operator is the associated method call on klass,
49
- # so client name maybe Price but true operator is price
59
+ # i.e client supplies name 'Price' in a spreadsheet,
60
+ # but true operator to call on klass is price
50
61
  #
51
62
  # col_types can typically be derived from klass.columns - set of ActiveRecord::ConnectionAdapters::Column
52
63
 
@@ -88,11 +99,10 @@ module DataShift
88
99
 
89
100
  # Return the operator's expected class name, if can be derived, else nil
90
101
  def operator_class_name()
91
- @operator_class_name ||= if(operator_for(:has_many) || operator_for(:belongs_to) || operator_for(:has_one))
92
- begin
93
- Kernel.const_get(operator.classify)
94
- operator.classify
95
- rescue; ""; end
102
+ @operator_class_name ||=
103
+ if(operator_for(:has_many) || operator_for(:belongs_to) || operator_for(:has_one))
104
+
105
+ get_operator_class.name
96
106
 
97
107
  elsif(@col_type)
98
108
  @col_type.type.to_s.classify
@@ -115,63 +125,31 @@ module DataShift
115
125
 
116
126
  private
117
127
 
118
- # Attempt to find the associated object via id, name, title ....
119
- def insistent_belongs_to( record, value )
120
-
121
- if( value.class == operator_class)
122
- record.send(operator) << value
123
- else
128
+ # Return the operator's expected class, if can be derived, else nil
129
+ # TODO rspec- can reflect_on_association ever actually fail & do we ever need to try ourselves (badly)
130
+ def get_operator_class()
131
+
132
+ if(operator_for(:has_many) || operator_for(:belongs_to) || operator_for(:has_one))
124
133
 
125
- @@insistent_find_by_list.each do |x|
126
- begin
127
- next unless operator_class.respond_to?( "find_by_#{x}" )
128
- item = operator_class.send( "find_by_#{x}", value)
129
- if(item)
130
- record.send(operator + '=', item)
131
- break
132
- end
133
- rescue => e
134
- logger.error "Failed to match belongs_to association #{value}"
135
- puts "ERROR: #{e.inspect}"
136
- if(x == Populator::insistent_method_list.last)
137
- raise "I'm sorry I have failed to assign [#{value}] to #{@assignment}" unless value.nil?
138
- end
139
- end
140
- end
141
- end
142
- end
134
+ result = klass.reflect_on_association(operator)
143
135
 
144
- # Attempt to find the associated object via id, name, title ....
145
- def insistent_has_many( record, value )
136
+ return result.klass if(result)
146
137
 
147
- if( value.class == operator_class)
148
- record.send(operator) << value
149
- else
150
- @@insistent_find_by_list.each do |x|
138
+ result = ModelMapper::class_from_string(operator.classify)
139
+
140
+ if(result.nil?)
151
141
  begin
152
- item = operator_class.send( "find_by_#{x}", value)
153
- if(item)
154
- record.send(operator) << item
155
- break
156
- end
142
+
143
+ first = klass.to_s.split('::').first
144
+ logger.debug "Trying to find operator class with Parent Namespace #{first}"
145
+
146
+ result = ModelMapper::const_get_from_string("#{first}::#{operator.classify}")
157
147
  rescue => e
158
- puts "ERROR: #{e.inspect}"
159
- if(x == Populator::insistent_method_list.last)
160
- raise "I'm sorry I have failed to assign [#{value}] to #{operator}" unless value.nil?
161
- end
148
+ logger.error("Failed to derive Class for #{operator} (#{@operator_type} - #{e.inspect}")
162
149
  end
163
150
  end
164
- end
165
- end
166
-
167
- private
168
-
169
- # Return the operator's expected class, if can be derived, else nil
170
- def get_operator_class()
171
- if(operator_for(:has_many) || operator_for(:belongs_to) || operator_for(:has_one))
172
- begin
173
- Kernel.const_get(operator.classify)
174
- rescue; nil; end
151
+
152
+ result
175
153
 
176
154
  elsif(@col_type)
177
155
  begin
@@ -6,6 +6,11 @@
6
6
  # Details:: This class provides info and access to groups of accessor methods,
7
7
  # grouped by AR model.
8
8
  #
9
+ # Stores complete collection of MethodDetail instances per mapped class.
10
+ # Provides high level find facilites to find a MethodDetail and to list out
11
+ # operators per type (has_one, has_many, belongs_to, instance_method etc)
12
+ # and all possible operators,
13
+
9
14
  require 'to_b'
10
15
 
11
16
  module DataShift
@@ -68,11 +73,22 @@ module DataShift
68
73
  get_list(type).collect { |md| md.operator }
69
74
  end
70
75
 
71
- alias_method(:get_list_of_operators, :get_list)
76
+ alias_method(:get_list_of_operators, :get_operators)
72
77
 
73
- def all_available_operators
78
+ def available_operators
74
79
  method_details_list.values.flatten.collect(&:operator)
75
80
  end
81
+
82
+ # A reverse map showing all operators with their associated 'type'
83
+ def available_operators_with_type
84
+ h = {}
85
+ method_details_list.each { |t, mds| mds.each do |v| h[v.operator] = t end }
86
+
87
+ # this is meant to be more efficient that Hash[h.sort]
88
+ sh = {}
89
+ h.keys.sort.each do |k| sh[k] = h[k] end
90
+ sh
91
+ end
76
92
 
77
93
  end
78
94
 
@@ -10,7 +10,8 @@ module DataShift
10
10
  class MethodDictionary
11
11
 
12
12
  include DataShift::Logging
13
-
13
+ extend DataShift::Logging
14
+
14
15
  # Return true if dictionary has been populated for klass
15
16
  def self.for?(klass)
16
17
  any = has_many[klass] || belongs_to[klass] || has_one[klass] || assignments[klass]
@@ -27,6 +28,8 @@ module DataShift
27
28
 
28
29
  raise "Cannot find operators supplied klass nil #{klass}" if(klass.nil?)
29
30
 
31
+ logger.debug("MethodDictionary - building operators information for #{klass}")
32
+
30
33
  # Find the has_many associations which can be populated via <<
31
34
  if( options[:reload] || has_many[klass].nil? )
32
35
  has_many[klass] = klass.reflect_on_all_associations(:has_many).map { |i| i.name.to_s }
@@ -63,8 +66,6 @@ module DataShift
63
66
 
64
67
  assignments[klass].uniq!
65
68
 
66
- #puts "\nDEBUG: DICT Setters\n#{assignments[klass]}\n"
67
-
68
69
  assignments[klass].each do |assign|
69
70
  column_types[klass] ||= {}
70
71
  column_def = klass.columns.find{ |col| col.name == assign }
@@ -95,10 +96,17 @@ module DataShift
95
96
 
96
97
  # Build a thorough and usable picture of the operators by building dictionary of our MethodDetail
97
98
  # objects which can be used to import/export data to objects of type 'klass'
98
- #
99
- def self.build_method_details( klass )
99
+ # Subsequent calls with same class will return existign mapping
100
+ # To over ride this behaviour, supply :force => true to force regeneration
101
+
102
+ def self.build_method_details( klass, options = {} )
103
+
104
+ return method_details_mgrs[klass] if(method_details_mgrs[klass] && !options[:force])
105
+
100
106
  method_details_mgr = MethodDetailsManager.new( klass )
101
-
107
+
108
+ method_details_mgrs[klass] = method_details_mgr
109
+
102
110
  assignments_for(klass).each do |n|
103
111
  method_details_mgr << MethodDetail.new(n, klass, n, :assignment, column_types[klass])
104
112
  end
@@ -115,7 +123,7 @@ module DataShift
115
123
  method_details_mgr << MethodDetail.new(n, klass, n, :belongs_to)
116
124
  end
117
125
 
118
- method_details_mgrs[klass] = method_details_mgr
126
+ method_details_mgr
119
127
 
120
128
  end
121
129
 
@@ -135,9 +143,18 @@ module DataShift
135
143
  name.gsub(' ', '_').underscore
136
144
  ]
137
145
  end
138
-
139
-
140
- # Find the proper format of name, appropriate call + column type for a given name.
146
+
147
+
148
+ # Dump out all available operators
149
+ #
150
+ def self.dump( klass )
151
+ method_details_mgr = get_method_details_mgr( klass )
152
+ #TODO
153
+ end
154
+
155
+
156
+ # For a client supplied name/header - find the operator i.e appropriate call + column type
157
+ #
141
158
  # e.g Given users entry in spread sheet check for pluralization, missing underscores etc
142
159
  #
143
160
  # If not nil, returned method can be used directly in for example klass.new.send( call, .... )
@@ -23,22 +23,6 @@ module DataShift
23
23
 
24
24
  attr_accessor :method_details, :missing_methods
25
25
 
26
-
27
- # As well as just the column name, support embedding find operators for that column
28
- # in the heading .. i.e Column header => 'BlogPosts:user_id'
29
- # ... association has many BlogPosts selected via find_by_user_id
30
- #
31
- # in the heading .. i.e Column header => 'BlogPosts:user_name:John Smith'
32
- # ... association has many BlogPosts selected via find_by_user_name("John Smith")
33
- #
34
- def self.column_delim
35
- @column_delim ||= ':'
36
- @column_delim
37
- end
38
-
39
- def self.set_column_delim(x) @column_delim = x; end
40
-
41
-
42
26
  def initialize
43
27
  @method_details = []
44
28
  end
@@ -47,10 +31,16 @@ module DataShift
47
31
  # Handles method names as defined by a user, from spreadsheets or file headers where the names
48
32
  # specified may not be exactly as required e.g handles capitalisation, white space, _ etc
49
33
  #
50
- # The header can also contain the fields to use in lookups, separated with MethodMapper::column_delim
51
- #
52
- # product:name or project:title or user:email
53
- #
34
+ # The header can also contain the fields to use in lookups, separated with Delimiters ::column_delim
35
+ # For example specify that lookups on has_one association called 'product', be performed using name'
36
+ # product:name
37
+ #
38
+ # The header can also contain a default value for the lookup field, again separated with Delimiters ::column_delim
39
+ #
40
+ # For example specify lookups on assoc called 'user', be performed using 'email' == 'test@blah.com'
41
+ #
42
+ # user:email:test@blah.com
43
+ #
54
44
  # Returns: Array of matching method_details, including nils for non matched items
55
45
  #
56
46
  # N.B Columns that could not be mapped are left in the array as NIL
@@ -59,15 +49,14 @@ module DataShift
59
49
  #
60
50
  # Other callers can simply call compact on the results if the index not important.
61
51
  #
62
- # The Methoddetails instance will contain a pointer to the column index from which it was mapped.
63
- #
52
+ # The MethodDetails instance will contain a pointer to the column index from which it was mapped.
64
53
  #
65
54
  # Options:
66
55
  #
67
- # [:force_inclusion] : List of columns that do not map to any operator but should be includeed in processing.
56
+ # [:force_inclusion] : List of columns that do not map to any operator but should be included in processing.
68
57
  #
69
58
  # This provides the opportunity for loaders to provide specific methods to handle these fields
70
- # when no direct operator is available on the modle or it's associations
59
+ # when no direct operator is available on the model or it's associations
71
60
  #
72
61
  # [:include_all] : Include all headers in processing - takes precedence of :force_inclusion
73
62
  #
@@ -88,7 +77,7 @@ module DataShift
88
77
  @method_details, @missing_methods = [], []
89
78
 
90
79
  columns.each_with_index do |col_data, col_index|
91
-
80
+
92
81
  raw_col_data = col_data.to_s
93
82
 
94
83
  if(raw_col_data.nil? or raw_col_data.empty?)
@@ -97,36 +86,55 @@ module DataShift
97
86
  next
98
87
  end
99
88
 
100
- raw_col_name, lookup = raw_col_data.split(MethodMapper::column_delim)
89
+ raw_col_name, where_field, where_value, *data = raw_col_data.split(Delimiters::column_delim)
101
90
 
102
91
  md = MethodDictionary::find_method_detail(klass, raw_col_name)
103
-
104
- if(md.nil?)
105
- #puts "DEBUG: Check Forced\n #{forced}.include?(#{raw_col_name}) #{forced.include?(raw_col_name.downcase)}"
106
-
92
+
93
+ if(md.nil?)
107
94
  if(options[:include_all] || forced.include?(raw_col_name.downcase))
95
+ logger.debug("Operator #{raw_col_name} not found but forced inclusion operative")
108
96
  md = MethodDictionary::add(klass, raw_col_name)
109
97
  end
110
98
  end
111
99
 
112
- if(md)
100
+ if(md)
101
+
113
102
  md.name = raw_col_name
114
103
  md.column_index = col_index
115
-
116
- # TODO we should check that the assoc on klass responds to the specified
117
- # lookup key now (nice n early)
118
- # active_record_helper = "find_by_#{lookup}"
119
- if(lookup)
120
- find_by, find_value = lookup.split(MethodMapper::column_delim)
121
- md.find_by_value = find_value
122
- md.find_by_operator = find_by # TODO and klass.x.respond_to?(active_record_helper))
123
- puts "DEBUG: Method Detail #{md.name};#{md.operator} : find_by_operator #{md.find_by_operator}"
104
+
105
+ # put data back as string for now - leave it to clients to decide what to do with it later
106
+ Populator::set_header_default_data(md.operator, data.join(Delimiters::column_delim))
107
+
108
+ if(where_field)
109
+ logger.info("Lookup query field [#{where_field}] - specified for association #{md.operator}")
110
+
111
+ md.find_by_value = where_value
112
+
113
+ # Example :
114
+ # Project:name:My Best Project
115
+ # User (klass) has_one project (operator) lookup by name (find_by_operator) == 'My Best Project' (find_by_value)
116
+ # User.project.where( :name => 'My Best Project')
117
+
118
+ # check the finder method name is a valid field on the actual association class
119
+
120
+ if(klass.reflect_on_association(md.operator) &&
121
+ klass.reflect_on_association(md.operator).klass.new.respond_to?(where_field))
122
+ md.find_by_operator = where_field
123
+ logger.info("Complex Lookup specified for [#{md.operator}] : on field [#{md.find_by_operator}] (optional value [#{md.find_by_value}])")
124
+ else
125
+ logger.warn("Find by operator [#{where_field}] Not Found on association [#{md.operator}] on Class #{klass.name} (#{md.inspect})")
126
+ logger.warn("Check column (#{md.column_index}) heading - e.g association field names are case sensitive")
127
+ # TODO - maybe derived loaders etc want this data for another purpose - should we stash elsewhere ?
128
+ end
124
129
  end
125
130
  else
126
131
  # TODO populate unmapped with a real MethodDetail that is 'null' and create is_nil
132
+ logger.warn("No operator or association found for Header #{raw_col_name}")
127
133
  @missing_methods << raw_col_name
128
134
  end
129
-
135
+
136
+ logger.debug("Column [#{col_data}] (#{col_index}) - mapped to :\n#{md.inspect}")
137
+
130
138
  @method_details << md
131
139
 
132
140
  end
@@ -1,27 +1,47 @@
1
- class ModelMapper
2
-
3
- # Helper to deal with string versions of modules/namespaced classes
4
- # Find and return the base class from a string.
5
- #
6
- # e.g "Spree::Property" returns the Spree::Property class
7
- # Raises exception if no such class found
8
- def self.const_get_from_string(str)
9
- str.to_s.split('::').inject(Object) do |mod, class_name|
10
- mod.const_get(class_name)
11
- end
12
- end
13
-
14
-
15
- # Similar to const_get_from_string except this version
16
- # returns nil if no such class found
17
- # Support modules e.g "Spree::Property"
18
- #
19
- def self.class_from_string( str )
1
+ module DataShift
2
+
3
+ class ModelMapper
4
+
5
+
6
+ def self.class_from_string_or_raise( klass )
7
+
8
+ ruby_klass = begin
9
+ # support modules e.g "Spree::Property")
10
+ ModelMapper::class_from_string(klass) #Kernel.const_get(model)
11
+ rescue NameError => e
12
+ puts e
13
+ raise Thor::Error.new("ERROR: No such Class [#{klass}] found - check valid model supplied")
14
+ end
15
+
16
+ raise NoSuchClassError.new("ERROR: No such Model [#{klass}] found - check valid model supplied") unless(ruby_klass)
17
+
18
+ ruby_klass
19
+ end
20
+
21
+
22
+ # Helper to deal with string versions of modules/namespaced classes
23
+ # Find and return the base class from a string.
24
+ #
25
+ # e.g "Spree::Property" returns the Spree::Property class
26
+ # Raises exception if no such class found
27
+ def self.const_get_from_string(str)
28
+ str.to_s.split('::').inject(Object) do |mod, class_name|
29
+ mod.const_get(class_name)
30
+ end
31
+ end
32
+
33
+
34
+ # Similar to const_get_from_string except this version
35
+ # returns nil if no such class found
36
+ # Support modules e.g "Spree::Property"
37
+ #
38
+ def self.class_from_string( str )
20
39
  begin
21
40
  ModelMapper::const_get_from_string(str.to_s) #Kernel.const_get(model)
22
- rescue NameError => e
23
- return nil
41
+ rescue
42
+ return nil
24
43
  end
44
+ end
45
+
25
46
  end
26
-
27
47
  end