active_importer 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +39 -2
- data/lib/active_importer/base.rb +21 -6
- data/lib/active_importer/version.rb +1 -1
- data/spec/active_importer/base_spec.rb +6 -7
- data/spec/stubs/data_model.rb +14 -4
- data/spec/stubs/employee.rb +1 -1
- metadata +4 -4
data/README.md
CHANGED
@@ -80,12 +80,16 @@ class EmployeeImporter < ActiveImporter::Base
|
|
80
80
|
|
81
81
|
attr_reader :row_count
|
82
82
|
|
83
|
-
column 'First name'
|
84
|
-
column 'Last name'
|
83
|
+
column 'First name'
|
84
|
+
column 'Last name'
|
85
85
|
column 'Department', :department do |department_name|
|
86
86
|
Department.find_by(name: department_name)
|
87
87
|
end
|
88
88
|
|
89
|
+
on :row_processing do
|
90
|
+
model.full_name = [row['First name'], row['Last name']].join(' ')
|
91
|
+
end
|
92
|
+
|
89
93
|
on :import_started do
|
90
94
|
@row_count = 0
|
91
95
|
end
|
@@ -117,6 +121,8 @@ The supported events are:
|
|
117
121
|
event is fired by an importer, none of its other events are ever fired.
|
118
122
|
- **import_started:** Fired once at the beginning of the data processing,
|
119
123
|
before the first row is processed.
|
124
|
+
- **row_processing:** Fired while the row is being processed to be imported
|
125
|
+
into a model instance.
|
120
126
|
- **row_processed:** Fired once for each row that has been processed,
|
121
127
|
regardless of whether it resulted in success or error.
|
122
128
|
- **row_success:** Fired once for each row that was imported successfully into
|
@@ -132,6 +138,37 @@ all the importer attributes and instance variables. Error-related events
|
|
132
138
|
(`:import_failed` and `:row_error`) pass to the blocks the instance of the
|
133
139
|
exception that provoked the error condition.
|
134
140
|
|
141
|
+
Additionally, all the `row_*` events have access to the `row` and `model`
|
142
|
+
variables, which reference the spreadsheet row being processed, and the model
|
143
|
+
object where the row data is being stored, respectively. This feature is
|
144
|
+
specifically useful for the `:row_processing` event handler, which is triggered
|
145
|
+
while a row is being processed, and before the corresponding data model is
|
146
|
+
saved. This allows to define any complex data-import logic that cannot be
|
147
|
+
expressed in terms of mapping a column to a data field.
|
148
|
+
|
149
|
+
### Selecting the model instance to import into
|
150
|
+
|
151
|
+
By default, the importer will attempt to generate a new model instance per row
|
152
|
+
processed. The importer can be instructed to update records instead, if they
|
153
|
+
already exist, instead of always attempting to generate a new one.
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
class EmployeeImporter
|
157
|
+
imports Employee
|
158
|
+
|
159
|
+
fetch_model do
|
160
|
+
Employee.where(first_name: row['First name'], last_name: row['Last name']).first_or_initialize
|
161
|
+
end
|
162
|
+
|
163
|
+
# ...
|
164
|
+
end
|
165
|
+
```
|
166
|
+
|
167
|
+
The code above specifies that, for each row, the importer should attempt to
|
168
|
+
find an existing model for the employee with the first and last name in the row
|
169
|
+
being processed. If this record exist, the row data will be used to update the
|
170
|
+
given model instance. Otherwise, a new employee record will be created.
|
171
|
+
|
135
172
|
## Contributing
|
136
173
|
|
137
174
|
1. Fork it
|
data/lib/active_importer/base.rb
CHANGED
@@ -26,6 +26,14 @@ module ActiveImporter
|
|
26
26
|
self.class.model_class
|
27
27
|
end
|
28
28
|
|
29
|
+
def self.fetch_model(&block)
|
30
|
+
@fetch_model_block = block
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.fetch_model_block
|
34
|
+
@fetch_model_block
|
35
|
+
end
|
36
|
+
|
29
37
|
def self.column(title, field = nil, &block)
|
30
38
|
title = title.strip
|
31
39
|
if columns[title]
|
@@ -45,6 +53,7 @@ module ActiveImporter
|
|
45
53
|
EVENTS = [
|
46
54
|
:row_success,
|
47
55
|
:row_error,
|
56
|
+
:row_processing,
|
48
57
|
:row_processed,
|
49
58
|
:import_started,
|
50
59
|
:import_finished,
|
@@ -77,6 +86,7 @@ module ActiveImporter
|
|
77
86
|
|
78
87
|
class << self
|
79
88
|
private :fire_event
|
89
|
+
private :fetch_model_block
|
80
90
|
end
|
81
91
|
|
82
92
|
#
|
@@ -104,8 +114,16 @@ module ActiveImporter
|
|
104
114
|
fire_event :import_failed, e
|
105
115
|
end
|
106
116
|
|
117
|
+
def fetch_model_block
|
118
|
+
self.class.send(:fetch_model_block)
|
119
|
+
end
|
120
|
+
|
107
121
|
def fetch_model
|
108
|
-
|
122
|
+
if fetch_model_block
|
123
|
+
self.instance_exec(&fetch_model_block)
|
124
|
+
else
|
125
|
+
model_class.new
|
126
|
+
end
|
109
127
|
end
|
110
128
|
|
111
129
|
def import
|
@@ -131,9 +149,6 @@ module ActiveImporter
|
|
131
149
|
row_errors.count
|
132
150
|
end
|
133
151
|
|
134
|
-
def hook
|
135
|
-
end
|
136
|
-
|
137
152
|
private
|
138
153
|
|
139
154
|
def columns
|
@@ -142,7 +157,7 @@ module ActiveImporter
|
|
142
157
|
|
143
158
|
def find_header_index
|
144
159
|
(1..@book.last_row).each do |index|
|
145
|
-
row = @book.row(index).map
|
160
|
+
row = @book.row(index).map { |cell| cell.to_s.strip }
|
146
161
|
return index if columns.keys.all? { |item| row.include?(item) }
|
147
162
|
end
|
148
163
|
return nil
|
@@ -182,7 +197,7 @@ module ActiveImporter
|
|
182
197
|
value = self.instance_exec(value, &transform) if transform
|
183
198
|
model[field_name] = value
|
184
199
|
end
|
185
|
-
|
200
|
+
fire_event :row_processing
|
186
201
|
end
|
187
202
|
|
188
203
|
def row_to_hash(row)
|
@@ -14,6 +14,7 @@ describe ActiveImporter::Base do
|
|
14
14
|
|
15
15
|
before do
|
16
16
|
expect(Roo::Spreadsheet).to receive(:open).and_return { Spreadsheet.new(spreadsheet_data) }
|
17
|
+
EmployeeImporter.instance_variable_set(:@fetch_model_block, nil)
|
17
18
|
end
|
18
19
|
|
19
20
|
it 'imports all data from the spreadsheet into the model' do
|
@@ -88,7 +89,7 @@ describe ActiveImporter::Base do
|
|
88
89
|
let(:spreadsheet_data) do
|
89
90
|
[
|
90
91
|
[],
|
91
|
-
['List of employees', '', 'Company Name'],
|
92
|
+
['List of employees', '', nil, 'Company Name'],
|
92
93
|
['Ordered by', 'Birth Date'],
|
93
94
|
['Name', 'Department', 'Birth Date', 'Manager'],
|
94
95
|
['John Doe', 'IT', '2013-10-25'],
|
@@ -102,19 +103,17 @@ describe ActiveImporter::Base do
|
|
102
103
|
end
|
103
104
|
|
104
105
|
describe '.fetch_model' do
|
105
|
-
let(:model) { Employee.new }
|
106
|
-
|
107
106
|
it 'controls what model instance is loaded for each given row' do
|
108
|
-
|
109
|
-
|
107
|
+
model = Employee.new
|
108
|
+
EmployeeImporter.fetch_model { model }
|
110
109
|
expect { EmployeeImporter.import('/dummy/file') }.to change(Employee, :count).by(1)
|
111
110
|
end
|
112
111
|
end
|
113
112
|
|
114
|
-
describe '
|
113
|
+
describe 'row_processing event' do
|
115
114
|
it 'allows the importer to modify the model for each row' do
|
116
115
|
expect(EmployeeImporter).to receive(:new).once.and_return(importer)
|
117
|
-
expect(importer).to receive(:
|
116
|
+
expect(importer).to receive(:row_processing).twice
|
118
117
|
EmployeeImporter.import('/dummy/file')
|
119
118
|
end
|
120
119
|
end
|
data/spec/stubs/data_model.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
class DataModel
|
2
|
-
@@count = 0
|
3
2
|
|
4
3
|
def self.count
|
5
|
-
|
4
|
+
@count ||= 0
|
6
5
|
end
|
7
6
|
|
8
7
|
attr_reader :errors
|
@@ -25,7 +24,7 @@ class DataModel
|
|
25
24
|
|
26
25
|
def save
|
27
26
|
if valid?
|
28
|
-
|
27
|
+
self.class.send(:increment_count) if @new_record
|
29
28
|
@new_record = false
|
30
29
|
true
|
31
30
|
else
|
@@ -43,10 +42,21 @@ class DataModel
|
|
43
42
|
|
44
43
|
def valid?
|
45
44
|
validate
|
46
|
-
|
45
|
+
errors.empty?
|
47
46
|
end
|
48
47
|
|
49
48
|
def validate
|
50
49
|
# ...
|
51
50
|
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def self.increment_count
|
55
|
+
count
|
56
|
+
@count += 1
|
57
|
+
end
|
58
|
+
|
59
|
+
class << self
|
60
|
+
private :increment_count
|
61
|
+
end
|
52
62
|
end
|
data/spec/stubs/employee.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_importer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-11-
|
12
|
+
date: 2013-11-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: roo
|
@@ -112,7 +112,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
112
|
version: '0'
|
113
113
|
segments:
|
114
114
|
- 0
|
115
|
-
hash: -
|
115
|
+
hash: -1076262899309802271
|
116
116
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
117
|
none: false
|
118
118
|
requirements:
|
@@ -121,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
121
|
version: '0'
|
122
122
|
segments:
|
123
123
|
- 0
|
124
|
-
hash: -
|
124
|
+
hash: -1076262899309802271
|
125
125
|
requirements: []
|
126
126
|
rubyforge_project:
|
127
127
|
rubygems_version: 1.8.23
|