datashift 0.15.0 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.markdown +91 -55
- data/VERSION +1 -1
- data/datashift.gemspec +8 -23
- data/lib/applications/jexcel_file.rb +1 -2
- data/lib/datashift.rb +34 -15
- data/lib/datashift/column_packer.rb +98 -34
- data/lib/datashift/data_transforms.rb +83 -0
- data/lib/datashift/delimiters.rb +58 -10
- data/lib/datashift/excel_base.rb +123 -0
- data/lib/datashift/exceptions.rb +45 -7
- data/lib/datashift/load_object.rb +25 -0
- data/lib/datashift/mapping_service.rb +91 -0
- data/lib/datashift/method_detail.rb +40 -62
- data/lib/datashift/method_details_manager.rb +18 -2
- data/lib/datashift/method_dictionary.rb +27 -10
- data/lib/datashift/method_mapper.rb +49 -41
- data/lib/datashift/model_mapper.rb +42 -22
- data/lib/datashift/populator.rb +258 -143
- data/lib/datashift/thor_base.rb +38 -0
- data/lib/exporters/csv_exporter.rb +57 -145
- data/lib/exporters/excel_exporter.rb +73 -60
- data/lib/generators/csv_generator.rb +65 -5
- data/lib/generators/generator_base.rb +69 -3
- data/lib/generators/mapping_generator.rb +112 -0
- data/lib/helpers/core_ext/csv_file.rb +33 -0
- data/lib/loaders/csv_loader.rb +41 -39
- data/lib/loaders/excel_loader.rb +130 -116
- data/lib/loaders/loader_base.rb +190 -146
- data/lib/loaders/paperclip/attachment_loader.rb +4 -4
- data/lib/loaders/paperclip/datashift_paperclip.rb +5 -3
- data/lib/loaders/paperclip/image_loading.rb +9 -7
- data/lib/loaders/reporter.rb +17 -8
- data/lib/thor/export.thor +12 -13
- data/lib/thor/generate.thor +1 -9
- data/lib/thor/import.thor +13 -24
- data/lib/thor/mapping.thor +65 -0
- data/spec/Gemfile +13 -11
- data/spec/Gemfile.lock +98 -93
- data/spec/csv_exporter_spec.rb +104 -99
- data/spec/csv_generator_spec.rb +159 -0
- data/spec/csv_loader_spec.rb +197 -16
- data/spec/datashift_spec.rb +9 -0
- data/spec/excel_exporter_spec.rb +149 -58
- data/spec/excel_generator_spec.rb +35 -44
- data/spec/excel_loader_spec.rb +196 -178
- data/spec/excel_spec.rb +8 -5
- data/spec/loader_base_spec.rb +47 -7
- data/spec/mapping_spec.rb +117 -0
- data/spec/method_dictionary_spec.rb +24 -11
- data/spec/method_mapper_spec.rb +5 -7
- data/spec/model_mapper_spec.rb +41 -0
- data/spec/paperclip_loader_spec.rb +3 -6
- data/spec/populator_spec.rb +48 -14
- data/spec/spec_helper.rb +85 -73
- data/spec/thor_spec.rb +40 -5
- metadata +93 -86
- 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
|
-
|
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
|
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
|
-
#
|
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 ||=
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
#
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
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
|
-
|
145
|
-
def insistent_has_many( record, value )
|
136
|
+
return result.klass if(result)
|
146
137
|
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
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
|
-
|
165
|
-
|
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, :
|
76
|
+
alias_method(:get_list_of_operators, :get_operators)
|
72
77
|
|
73
|
-
def
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
51
|
-
#
|
52
|
-
# product:name
|
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
|
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
|
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
|
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,
|
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
|
-
#
|
117
|
-
|
118
|
-
|
119
|
-
if(
|
120
|
-
|
121
|
-
|
122
|
-
md.
|
123
|
-
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
23
|
-
|
41
|
+
rescue
|
42
|
+
return nil
|
24
43
|
end
|
44
|
+
end
|
45
|
+
|
25
46
|
end
|
26
|
-
|
27
47
|
end
|