active_importer 0.2.1 → 0.2.2
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.
- 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
|