datashift 0.15.0 → 0.16.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 (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