active_importer 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +37 -0
- data/lib/active_importer/base.rb +44 -2
- data/lib/active_importer/version.rb +1 -1
- data/spec/active_importer/base_spec.rb +76 -1
- data/spec/stubs/employee.rb +8 -0
- data/spec/stubs/spreadsheet.rb +16 -2
- metadata +4 -4
data/README.md
CHANGED
@@ -123,6 +123,8 @@ The supported events are:
|
|
123
123
|
before the first row is processed.
|
124
124
|
- **row_processing:** Fired while the row is being processed to be imported
|
125
125
|
into a model instance.
|
126
|
+
- **row_skipped:** Fired once for each row that matches the `skip_rows_if`
|
127
|
+
condition, if any.
|
126
128
|
- **row_processed:** Fired once for each row that has been processed,
|
127
129
|
regardless of whether it resulted in success or error.
|
128
130
|
- **row_success:** Fired once for each row that was imported successfully into
|
@@ -130,6 +132,8 @@ The supported events are:
|
|
130
132
|
- **row_error:** Fired once for each row that was **not** imported successfully
|
131
133
|
into the data model.
|
132
134
|
- **import_finished:** Fired once **after** all rows have been processed.
|
135
|
+
- **import_aborted:** Fired once if the import process is aborted by invoking
|
136
|
+
`abort!`.
|
133
137
|
|
134
138
|
More than one block of code can be provided for each of these events, and they
|
135
139
|
will all be invoked in the same order in which they were declared. All blocks
|
@@ -169,6 +173,39 @@ find an existing model for the employee with the first and last name in the row
|
|
169
173
|
being processed. If this record exist, the row data will be used to update the
|
170
174
|
given model instance. Otherwise, a new employee record will be created.
|
171
175
|
|
176
|
+
### Selecting the sheet to get data from
|
177
|
+
|
178
|
+
Spreadsheet files often have more than one sheet of data, so it is desirable to
|
179
|
+
select which sheet to use when importing.
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
class EmployeeImporter
|
183
|
+
imports Employee
|
184
|
+
|
185
|
+
sheet "Employees"
|
186
|
+
|
187
|
+
# ...
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
The importer defined above specifies that data should be read from a sheet
|
192
|
+
named "Employees". By default an importer will read from the first sheet in
|
193
|
+
the spreadsheet.
|
194
|
+
|
195
|
+
Also, sheets can be specified by name or by index, starting by 1, which is the
|
196
|
+
first sheet. For instance, the following importer will read data from the
|
197
|
+
third sheet, no matter what's its name.
|
198
|
+
|
199
|
+
```ruby
|
200
|
+
class EmployeeImporter
|
201
|
+
imports Employee
|
202
|
+
|
203
|
+
sheet 3
|
204
|
+
|
205
|
+
# ...
|
206
|
+
end
|
207
|
+
```
|
208
|
+
|
172
209
|
## Contributing
|
173
210
|
|
174
211
|
1. Fork it
|
data/lib/active_importer/base.rb
CHANGED
@@ -7,6 +7,12 @@ module ActiveImporter
|
|
7
7
|
# DSL and class variables
|
8
8
|
#
|
9
9
|
|
10
|
+
@aborted = false
|
11
|
+
|
12
|
+
def abort!
|
13
|
+
@aborted = true
|
14
|
+
end
|
15
|
+
|
10
16
|
@model_class = nil
|
11
17
|
@columns = {}
|
12
18
|
|
@@ -26,6 +32,10 @@ module ActiveImporter
|
|
26
32
|
self.class.model_class
|
27
33
|
end
|
28
34
|
|
35
|
+
def self.sheet(index)
|
36
|
+
@sheet_index = index
|
37
|
+
end
|
38
|
+
|
29
39
|
def self.fetch_model(&block)
|
30
40
|
@fetch_model_block = block
|
31
41
|
end
|
@@ -34,6 +44,14 @@ module ActiveImporter
|
|
34
44
|
@fetch_model_block
|
35
45
|
end
|
36
46
|
|
47
|
+
def self.skip_rows_if(&block)
|
48
|
+
@skip_rows_block = block
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.skip_rows_block
|
52
|
+
@skip_rows_block
|
53
|
+
end
|
54
|
+
|
37
55
|
def self.column(title, field = nil, &block)
|
38
56
|
title = title.strip
|
39
57
|
if columns[title]
|
@@ -54,10 +72,12 @@ module ActiveImporter
|
|
54
72
|
:row_success,
|
55
73
|
:row_error,
|
56
74
|
:row_processing,
|
75
|
+
:row_skipped,
|
57
76
|
:row_processed,
|
58
77
|
:import_started,
|
59
78
|
:import_finished,
|
60
79
|
:import_failed,
|
80
|
+
:import_aborted,
|
61
81
|
]
|
62
82
|
|
63
83
|
def self.event_handlers
|
@@ -103,6 +123,7 @@ module ActiveImporter
|
|
103
123
|
@context = options.delete(:context)
|
104
124
|
|
105
125
|
@book = Roo::Spreadsheet.open(file, options)
|
126
|
+
load_sheet
|
106
127
|
load_header
|
107
128
|
|
108
129
|
@data_row_indices = ((@header_index+1)..@book.last_row)
|
@@ -132,7 +153,15 @@ module ActiveImporter
|
|
132
153
|
@data_row_indices.each do |index|
|
133
154
|
@row_index = index
|
134
155
|
@row = row_to_hash @book.row(index)
|
156
|
+
if skip_row?
|
157
|
+
fire_event :row_skipped
|
158
|
+
next
|
159
|
+
end
|
135
160
|
import_row
|
161
|
+
if @aborted
|
162
|
+
fire_event :import_aborted
|
163
|
+
break
|
164
|
+
end
|
136
165
|
end
|
137
166
|
fire_event :import_finished
|
138
167
|
end
|
@@ -157,6 +186,19 @@ module ActiveImporter
|
|
157
186
|
self.class.columns
|
158
187
|
end
|
159
188
|
|
189
|
+
def skip_row?
|
190
|
+
block = self.class.skip_rows_block
|
191
|
+
block.nil? || self.instance_exec(&block)
|
192
|
+
end
|
193
|
+
|
194
|
+
def load_sheet
|
195
|
+
sheet_index = self.class.instance_variable_get(:@sheet_index)
|
196
|
+
if sheet_index
|
197
|
+
sheet_index = @book.sheets[sheet_index-1] if sheet_index.is_a?(Fixnum)
|
198
|
+
@book.default_sheet = sheet_index.to_s
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
160
202
|
def find_header_index
|
161
203
|
(1..@book.last_row).each do |index|
|
162
204
|
row = @book.row(index).map { |cell| cell.to_s.strip }
|
@@ -178,7 +220,7 @@ module ActiveImporter
|
|
178
220
|
begin
|
179
221
|
@model = fetch_model
|
180
222
|
build_model
|
181
|
-
model.save!
|
223
|
+
model.save! unless @aborted
|
182
224
|
rescue => e
|
183
225
|
@row_errors << { row_index: row_index, error_message: e.message }
|
184
226
|
fire_event :row_error, e
|
@@ -197,7 +239,7 @@ module ActiveImporter
|
|
197
239
|
field_name = column_def[:field_name]
|
198
240
|
transform = column_def[:transform]
|
199
241
|
value = self.instance_exec(value, &transform) if transform
|
200
|
-
model
|
242
|
+
model.send("#{field_name}=", value)
|
201
243
|
end
|
202
244
|
fire_event :row_processing
|
203
245
|
end
|
@@ -24,8 +24,9 @@ describe ActiveImporter::Base do
|
|
24
24
|
let(:importer) { EmployeeImporter.new('/dummy/file') }
|
25
25
|
|
26
26
|
before do
|
27
|
-
expect(Roo::Spreadsheet).to receive(:open).and_return { Spreadsheet.new(spreadsheet_data) }
|
27
|
+
expect(Roo::Spreadsheet).to receive(:open).at_least(:once).and_return { Spreadsheet.new(spreadsheet_data) }
|
28
28
|
EmployeeImporter.instance_variable_set(:@fetch_model_block, nil)
|
29
|
+
EmployeeImporter.instance_variable_set(:@sheet_index, nil)
|
29
30
|
end
|
30
31
|
|
31
32
|
it 'imports all data from the spreadsheet into the model' do
|
@@ -153,4 +154,78 @@ describe ActiveImporter::Base do
|
|
153
154
|
EmployeeImporter.import('/dummy/file')
|
154
155
|
end
|
155
156
|
end
|
157
|
+
|
158
|
+
context 'when spreadsheet has multiple sheets' do
|
159
|
+
let(:spreadsheet_data) do
|
160
|
+
{
|
161
|
+
"Employees" => [
|
162
|
+
[' Name ', 'Birth Date', 'Department', 'Manager'],
|
163
|
+
['John Doe', '2013-10-25', 'IT'],
|
164
|
+
['Jane Doe', '2013-10-26', 'Sales'],
|
165
|
+
],
|
166
|
+
"Outstanding employees" => [
|
167
|
+
[' Name ', 'Birth Date', 'Department', 'Manager'],
|
168
|
+
['Jane Doe', '2013-10-26', 'Sales'],
|
169
|
+
],
|
170
|
+
}
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'uses the first sheet by default' do
|
174
|
+
expect { EmployeeImporter.import('/dummy/file') }.to change(Employee, :count).by(2)
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'uses another sheet if instructed to do so' do
|
178
|
+
EmployeeImporter.sheet 1
|
179
|
+
expect { EmployeeImporter.import('/dummy/file') }.to change(Employee, :count).by(2)
|
180
|
+
EmployeeImporter.sheet "Outstanding employees"
|
181
|
+
expect { EmployeeImporter.import('/dummy/file') }.to change(Employee, :count).by(1)
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'fails if the specified sheet cannot be found' do
|
185
|
+
expect_any_instance_of(EmployeeImporter).to receive(:import_failed)
|
186
|
+
EmployeeImporter.sheet 5
|
187
|
+
EmployeeImporter.import('/dummy/file')
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
describe '#abort!' do
|
192
|
+
let(:spreadsheet_data) do
|
193
|
+
[
|
194
|
+
[' Name ', 'Birth Date', 'Department', 'Manager'],
|
195
|
+
['John Doe', '2013-10-25', 'IT'],
|
196
|
+
['Abort', '2013-10-25', 'IT'],
|
197
|
+
['Jane Doe', '2013-10-26', 'Sales'],
|
198
|
+
]
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'causes the import process to abort without processing any more rows' do
|
202
|
+
expect { EmployeeImporter.import('/dummy/file') }.to change(Employee, :count).by(1)
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'does not report an error for the row where the abortion occured' do
|
206
|
+
expect(importer).not_to receive(:row_error)
|
207
|
+
EmployeeImporter.import('/dummy/file')
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
describe '.skip_rows_if' do
|
212
|
+
let(:spreadsheet_data) do
|
213
|
+
[
|
214
|
+
[' Name ', 'Birth Date', 'Department', 'Manager'],
|
215
|
+
['Skip', '2013-10-25', 'IT'],
|
216
|
+
['John Doe', '2013-10-25', 'IT'],
|
217
|
+
['Jane Doe', '2013-10-26', 'Sales'],
|
218
|
+
]
|
219
|
+
end
|
220
|
+
|
221
|
+
it 'skips processing the current row' do
|
222
|
+
expect { EmployeeImporter.import('/dummy/file') }.to change(Employee, :count).by(2)
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'invokes event :row_skipped for each skipped row' do
|
226
|
+
expect(EmployeeImporter).to receive(:new).once.and_return(importer)
|
227
|
+
expect(importer).to receive(:row_skipped).once
|
228
|
+
EmployeeImporter.import('/dummy/file')
|
229
|
+
end
|
230
|
+
end
|
156
231
|
end
|
data/spec/stubs/employee.rb
CHANGED
@@ -28,6 +28,14 @@ class EmployeeImporter < EmployeeBaseImporter
|
|
28
28
|
find_department(value)
|
29
29
|
end
|
30
30
|
|
31
|
+
on :row_processing do
|
32
|
+
abort! if row['Name'] == 'Abort'
|
33
|
+
end
|
34
|
+
|
35
|
+
skip_rows_if do
|
36
|
+
row['Name'] == 'Skip'
|
37
|
+
end
|
38
|
+
|
31
39
|
def find_department(name)
|
32
40
|
name.length # Quick dummy way to get an integer out of a string
|
33
41
|
end
|
data/spec/stubs/spreadsheet.rb
CHANGED
@@ -1,13 +1,27 @@
|
|
1
1
|
class Spreadsheet
|
2
2
|
def initialize(data)
|
3
|
+
data = { "Default" => data } unless data.is_a?(Hash)
|
3
4
|
@data = data
|
4
5
|
end
|
5
6
|
|
7
|
+
def sheets
|
8
|
+
@sheets ||= @data.keys
|
9
|
+
end
|
10
|
+
|
11
|
+
def default_sheet
|
12
|
+
@default_sheet ||= sheets.first
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_sheet=(value)
|
16
|
+
raise "Invalid sheet '#{value}'" unless sheets.include?(value)
|
17
|
+
@default_sheet = value
|
18
|
+
end
|
19
|
+
|
6
20
|
def last_row
|
7
|
-
@data.count
|
21
|
+
@data[default_sheet].count
|
8
22
|
end
|
9
23
|
|
10
24
|
def row(index)
|
11
|
-
@data[index-1]
|
25
|
+
@data[default_sheet][index-1]
|
12
26
|
end
|
13
27
|
end
|
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.2.
|
4
|
+
version: 0.2.2
|
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-19 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: -4188845756724848083
|
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: -4188845756724848083
|
125
125
|
requirements: []
|
126
126
|
rubyforge_project:
|
127
127
|
rubygems_version: 1.8.23
|