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 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
@@ -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[field_name] = value
242
+ model.send("#{field_name}=", value)
201
243
  end
202
244
  fire_event :row_processing
203
245
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveImporter
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.2"
3
3
  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
@@ -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
@@ -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.1
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-13 00:00:00.000000000 Z
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: -3865447106793864810
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: -3865447106793864810
124
+ hash: -4188845756724848083
125
125
  requirements: []
126
126
  rubyforge_project:
127
127
  rubygems_version: 1.8.23