active_importer 0.2.4 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +24 -154
- data/active_importer.gemspec +3 -0
- data/lib/active_importer/base.rb +76 -18
- data/lib/active_importer/version.rb +1 -1
- data/spec/active_importer/base_spec.rb +116 -4
- data/spec/spec_helper.rb +5 -0
- data/spec/support/active_record/models.rb +5 -0
- data/spec/support/active_record/schema.rb +17 -0
- data/spec/{stubs/employee.rb → support/employee_importer.rb} +2 -11
- data/spec/{stubs → support}/spreadsheet.rb +0 -0
- metadata +53 -39
- data/spec/stubs/data_model.rb +0 -62
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a6daa340c010143c7efa4f1f94cb5cb1778c13bf
|
4
|
+
data.tar.gz: a45ede6787d0fee8563973292bd2456133b61ae8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 974291103c95864dc5409fa19544fd135ff1485b559193f779a7243e6abd42185ff80d602a6d868b9cc625b7d265a6e06e2e2cf5984e395645e7e4b845631931
|
7
|
+
data.tar.gz: 608cf19443e5e470d600cb41f0e966cacc53c563a5b68ebaa3e6d93ad4116982e9fb5273fba199ae414fbf43211c7b25894459ce83b55b040da0b689e7d0b0a5
|
data/README.md
CHANGED
@@ -48,168 +48,38 @@ columns declared. Any extra columns are ignored. Any errors while processing
|
|
48
48
|
the data file does not interrupt the whole process. Instead, errors are
|
49
49
|
notified via some callbacks defined in the importer (see below).
|
50
50
|
|
51
|
-
|
51
|
+
## Documentation
|
52
52
|
|
53
|
-
|
54
|
-
to the
|
55
|
-
following formats are supported:
|
53
|
+
For mote detailed information about the different aspects of importing data
|
54
|
+
with `active_importer`, refer to the following sections in the [wiki]().
|
56
55
|
|
57
|
-
|
58
|
-
* Excel
|
59
|
-
* Google spreadsheets
|
60
|
-
* Excelx
|
61
|
-
* LibreOffice
|
62
|
-
* CSV
|
56
|
+
[wiki]: https://github.com/continuum/active_importer/wiki
|
63
57
|
|
64
|
-
|
65
|
-
matches the expect header column, which should contain header cells for all the
|
66
|
-
columns declared in the importer. If no such row is found, the spreadsheet
|
67
|
-
processing fails without importing any data.
|
58
|
+
### Getting started
|
68
59
|
|
69
|
-
|
70
|
-
|
60
|
+
* [Understanding how spreadsheets are parsed](https://github.com/continuum/active_importer/wiki/Understanding-how-spreadsheets-are-parsed)
|
61
|
+
* [Mapping columns to attributes](https://github.com/continuum/active_importer/wiki/Mapping-columns-to-attributes)
|
71
62
|
|
72
|
-
###
|
63
|
+
### Diving in
|
73
64
|
|
74
|
-
|
75
|
-
|
65
|
+
* [Custom data processing](https://github.com/continuum/active_importer/wiki/Custom-data-processing)
|
66
|
+
* [Helper methods](https://github.com/continuum/active_importer/wiki/Helper-methods)
|
67
|
+
* [File extension and supported formats](https://github.com/continuum/active_importer/wiki/File-extension-and-supported-formats)
|
68
|
+
* [Passing custom parameters](https://github.com/continuum/active_importer/wiki/Custom-parameters)
|
69
|
+
* [Events and callbacks](https://github.com/continuum/active_importer/wiki/Callbacks)
|
70
|
+
* [Selecting the model instance to import into (Update instead of create)](https://github.com/continuum/active_importer/wiki/Update-instead-of-create)
|
71
|
+
* [Error handling](https://github.com/continuum/active_importer/wiki/Error-handling)
|
72
|
+
* [Selecting the sheet to get data from](https://github.com/continuum/active_importer/wiki/Selecting-the-sheet-to-work-with)
|
73
|
+
* [Skipping rows](https://github.com/continuum/active_importer/wiki/Skipping-rows)
|
76
74
|
|
77
|
-
|
78
|
-
class EmployeeImporter < ActiveImporter::Base
|
79
|
-
imports Employee
|
80
|
-
|
81
|
-
attr_reader :row_count
|
82
|
-
|
83
|
-
column 'First name'
|
84
|
-
column 'Last name'
|
85
|
-
column 'Department', :department do |department_name|
|
86
|
-
Department.find_by(name: department_name)
|
87
|
-
end
|
88
|
-
|
89
|
-
on :row_processing do
|
90
|
-
model.full_name = [row['First name'], row['Last name']].join(' ')
|
91
|
-
end
|
92
|
-
|
93
|
-
on :import_started do
|
94
|
-
@row_count = 0
|
95
|
-
end
|
96
|
-
|
97
|
-
on :row_processed do
|
98
|
-
@row_count += 1
|
99
|
-
end
|
100
|
-
|
101
|
-
on :import_finished do
|
102
|
-
send_notification("Data imported successfully!")
|
103
|
-
end
|
75
|
+
### Advanced features
|
104
76
|
|
105
|
-
|
106
|
-
|
107
|
-
end
|
108
|
-
|
109
|
-
private
|
110
|
-
|
111
|
-
def send_notification(message)
|
112
|
-
# ...
|
113
|
-
end
|
114
|
-
end
|
115
|
-
```
|
116
|
-
|
117
|
-
The supported events are:
|
118
|
-
|
119
|
-
- **import_failed:** Fired once **before** the beginning of the data
|
120
|
-
processing, if the input data cannot be processed for some reason. If this
|
121
|
-
event is fired by an importer, none of its other events are ever fired.
|
122
|
-
- **import_started:** Fired once at the beginning of the data processing,
|
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.
|
126
|
-
- **row_skipped:** Fired once for each row that matches the `skip_rows_if`
|
127
|
-
condition, if any.
|
128
|
-
- **row_processed:** Fired once for each row that has been processed,
|
129
|
-
regardless of whether it resulted in success or error.
|
130
|
-
- **row_success:** Fired once for each row that was imported successfully into
|
131
|
-
the data model.
|
132
|
-
- **row_error:** Fired once for each row that was **not** imported successfully
|
133
|
-
into the data model.
|
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!`.
|
137
|
-
|
138
|
-
More than one block of code can be provided for each of these events, and they
|
139
|
-
will all be invoked in the same order in which they were declared. All blocks
|
140
|
-
are executed in the context of the importer instance, so they have access to
|
141
|
-
all the importer attributes and instance variables. Error-related events
|
142
|
-
(`:import_failed` and `:row_error`) pass to the blocks the instance of the
|
143
|
-
exception that provoked the error condition.
|
144
|
-
|
145
|
-
Additionally, all the `row_*` events have access to the `row` and `model`
|
146
|
-
variables, which reference the spreadsheet row being processed, and the model
|
147
|
-
object where the row data is being stored, respectively. This feature is
|
148
|
-
specifically useful for the `:row_processing` event handler, which is triggered
|
149
|
-
while a row is being processed, and before the corresponding data model is
|
150
|
-
saved. This allows to define any complex data-import logic that cannot be
|
151
|
-
expressed in terms of mapping a column to a data field.
|
152
|
-
|
153
|
-
### Selecting the model instance to import into
|
154
|
-
|
155
|
-
By default, the importer will attempt to generate a new model instance per row
|
156
|
-
processed. The importer can be instructed to update records instead, if they
|
157
|
-
already exist, instead of always attempting to generate a new one.
|
158
|
-
|
159
|
-
```ruby
|
160
|
-
class EmployeeImporter
|
161
|
-
imports Employee
|
162
|
-
|
163
|
-
fetch_model do
|
164
|
-
Employee.where(first_name: row['First name'], last_name: row['Last name']).first_or_initialize
|
165
|
-
end
|
166
|
-
|
167
|
-
# ...
|
168
|
-
end
|
169
|
-
```
|
170
|
-
|
171
|
-
The code above specifies that, for each row, the importer should attempt to
|
172
|
-
find an existing model for the employee with the first and last name in the row
|
173
|
-
being processed. If this record exist, the row data will be used to update the
|
174
|
-
given model instance. Otherwise, a new employee record will be created.
|
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
|
-
```
|
77
|
+
* [Aborting the import process](https://github.com/continuum/active_importer/wiki/Aborting-the-import-process)
|
78
|
+
* [Transactional importers](https://github.com/continuum/active_importer/wiki/Transactional-importers)
|
208
79
|
|
209
80
|
## Contributing
|
210
81
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
5. Create new Pull Request
|
82
|
+
Contributions are welcome! Take a look at our [contributions guide][] for
|
83
|
+
details.
|
84
|
+
|
85
|
+
[contributions guide]: https://github.com/continuum/active_importer/wiki/Contributing
|
data/active_importer.gemspec
CHANGED
@@ -23,4 +23,7 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.add_development_dependency "bundler", "~> 1.3"
|
24
24
|
spec.add_development_dependency "rake"
|
25
25
|
spec.add_development_dependency "rspec"
|
26
|
+
|
27
|
+
spec.add_development_dependency "sqlite3"
|
28
|
+
spec.add_development_dependency "activerecord"
|
26
29
|
end
|
data/lib/active_importer/base.rb
CHANGED
@@ -53,18 +53,64 @@ module ActiveImporter
|
|
53
53
|
@skip_rows_block
|
54
54
|
end
|
55
55
|
|
56
|
-
def self.column(title, field = nil, &block)
|
56
|
+
def self.column(title, field = nil, options = nil, &block)
|
57
57
|
title = title.strip
|
58
58
|
if columns[title]
|
59
59
|
raise "Duplicate importer column '#{title}'"
|
60
60
|
end
|
61
|
-
|
61
|
+
|
62
|
+
if field.is_a?(Hash)
|
63
|
+
raise "Invalid column '#{title}': expected a single set of options" unless options.nil?
|
64
|
+
options = field
|
65
|
+
field = nil
|
66
|
+
else
|
67
|
+
options ||= {}
|
68
|
+
end
|
69
|
+
|
70
|
+
if field.nil? && block_given?
|
71
|
+
raise "Invalid column '#{title}': must have a corresponding attribute, or it shouldn't have a block"
|
72
|
+
end
|
73
|
+
|
74
|
+
columns[title] = {
|
75
|
+
field_name: field,
|
76
|
+
transform: block,
|
77
|
+
optional: !!options[:optional],
|
78
|
+
}
|
62
79
|
end
|
63
80
|
|
64
81
|
def self.import(file, options = {})
|
65
82
|
new(file, options).import
|
66
83
|
end
|
67
84
|
|
85
|
+
#
|
86
|
+
# Transactions
|
87
|
+
#
|
88
|
+
|
89
|
+
def self.transactional(flag = true)
|
90
|
+
if flag
|
91
|
+
raise "Model class does not support transactions" unless @model_class.respond_to?(:transaction)
|
92
|
+
end
|
93
|
+
@transactional = !!flag
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.transactional?
|
97
|
+
@transactional || false
|
98
|
+
end
|
99
|
+
|
100
|
+
def transactional?
|
101
|
+
@transactional || self.class.transactional?
|
102
|
+
end
|
103
|
+
|
104
|
+
def transaction
|
105
|
+
if transactional?
|
106
|
+
model_class.transaction { yield }
|
107
|
+
else
|
108
|
+
yield
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private :transaction
|
113
|
+
|
68
114
|
#
|
69
115
|
# Callbacks
|
70
116
|
#
|
@@ -117,11 +163,14 @@ module ActiveImporter
|
|
117
163
|
attr_reader :header, :row, :model
|
118
164
|
attr_reader :row_count, :row_index
|
119
165
|
attr_reader :row_errors
|
120
|
-
attr_reader :
|
166
|
+
attr_reader :params
|
121
167
|
|
122
168
|
def initialize(file, options = {})
|
123
169
|
@row_errors = []
|
124
|
-
@
|
170
|
+
@params = options.delete(:params)
|
171
|
+
@transactional = options.fetch(:transactional, self.class.transactional?)
|
172
|
+
|
173
|
+
raise "Importer is declared transactional at the class level" if !@transactional && self.class.transactional?
|
125
174
|
|
126
175
|
@book = Roo::Spreadsheet.open(file, options)
|
127
176
|
load_sheet
|
@@ -134,6 +183,7 @@ module ActiveImporter
|
|
134
183
|
@row_count = 0
|
135
184
|
@row_index = 1
|
136
185
|
fire_event :import_failed, e
|
186
|
+
raise
|
137
187
|
end
|
138
188
|
|
139
189
|
def fetch_model_block
|
@@ -149,21 +199,27 @@ module ActiveImporter
|
|
149
199
|
end
|
150
200
|
|
151
201
|
def import
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
@
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
202
|
+
transaction do
|
203
|
+
return if @book.nil?
|
204
|
+
fire_event :import_started
|
205
|
+
@data_row_indices.each do |index|
|
206
|
+
@row_index = index
|
207
|
+
@row = row_to_hash @book.row(index)
|
208
|
+
if skip_row?
|
209
|
+
fire_event :row_skipped
|
210
|
+
next
|
211
|
+
end
|
212
|
+
import_row
|
213
|
+
if aborted?
|
214
|
+
fire_event :import_aborted, @abort_message
|
215
|
+
break
|
216
|
+
end
|
165
217
|
end
|
166
218
|
end
|
219
|
+
rescue => e
|
220
|
+
fire_event :import_aborted, e.message
|
221
|
+
raise
|
222
|
+
ensure
|
167
223
|
fire_event :import_finished
|
168
224
|
end
|
169
225
|
|
@@ -201,9 +257,10 @@ module ActiveImporter
|
|
201
257
|
end
|
202
258
|
|
203
259
|
def find_header_index
|
260
|
+
required_column_keys = columns.keys.reject { |title| columns[title][:optional] }
|
204
261
|
(1..@book.last_row).each do |index|
|
205
262
|
row = @book.row(index).map { |cell| cell.to_s.strip }
|
206
|
-
return index if
|
263
|
+
return index if required_column_keys.all? { |item| row.include?(item) }
|
207
264
|
end
|
208
265
|
return nil
|
209
266
|
end
|
@@ -225,6 +282,7 @@ module ActiveImporter
|
|
225
282
|
rescue => e
|
226
283
|
@row_errors << { row_index: row_index, error_message: e.message }
|
227
284
|
fire_event :row_error, e
|
285
|
+
raise if transactional?
|
228
286
|
return false
|
229
287
|
end
|
230
288
|
fire_event :row_success
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'stubs/employee'
|
3
2
|
|
4
3
|
describe ActiveImporter::Base do
|
5
4
|
let(:spreadsheet_data) do
|
@@ -24,9 +23,16 @@ describe ActiveImporter::Base do
|
|
24
23
|
let(:importer) { EmployeeImporter.new('/dummy/file') }
|
25
24
|
|
26
25
|
before do
|
27
|
-
|
26
|
+
allow(Roo::Spreadsheet).to receive(:open).at_least(:once).and_return { Spreadsheet.new(spreadsheet_data) }
|
28
27
|
EmployeeImporter.instance_variable_set(:@fetch_model_block, nil)
|
29
28
|
EmployeeImporter.instance_variable_set(:@sheet_index, nil)
|
29
|
+
EmployeeImporter.transactional(false)
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '.column' do
|
33
|
+
it 'does not allow a column with block and no attribute' do
|
34
|
+
expect { EmployeeImporter.column('Dummy') {} }.to raise_error
|
35
|
+
end
|
30
36
|
end
|
31
37
|
|
32
38
|
it 'imports all data from the spreadsheet into the model' do
|
@@ -48,6 +54,28 @@ describe ActiveImporter::Base do
|
|
48
54
|
EmployeeImporter.import('/dummy/file')
|
49
55
|
end
|
50
56
|
|
57
|
+
it 'can receive custom parameters via the `params` option' do
|
58
|
+
importer = EmployeeImporter.new('/dummy/file', params: 'anything')
|
59
|
+
expect(importer.params).to eql('anything')
|
60
|
+
end
|
61
|
+
|
62
|
+
context do
|
63
|
+
let(:spreadsheet_data) do
|
64
|
+
[
|
65
|
+
[' Name ', 'Birth Date', 'Department', 'Unused', 'Manager'],
|
66
|
+
['Mary', '2013-10-25', 'IT', 'hello'],
|
67
|
+
['John', '2013-10-26', 'Sales', 'world'],
|
68
|
+
]
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'processes optional columns when present' do
|
72
|
+
expect(EmployeeImporter).to receive(:new).once.and_return(importer)
|
73
|
+
expect {
|
74
|
+
EmployeeImporter.import('/dummy/file')
|
75
|
+
}.to change(Employee.where.not(unused_field: nil), :count).by(2)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
51
79
|
context do
|
52
80
|
let(:spreadsheet_data) { spreadsheet_data_with_errors }
|
53
81
|
|
@@ -112,7 +140,9 @@ describe ActiveImporter::Base do
|
|
112
140
|
|
113
141
|
it 'notifies the failure' do
|
114
142
|
expect_any_instance_of(EmployeeImporter).to receive(:import_failed)
|
115
|
-
|
143
|
+
expect {
|
144
|
+
EmployeeImporter.import('/dummy/file')
|
145
|
+
}.to raise_error
|
116
146
|
end
|
117
147
|
end
|
118
148
|
|
@@ -184,7 +214,9 @@ describe ActiveImporter::Base do
|
|
184
214
|
it 'fails if the specified sheet cannot be found' do
|
185
215
|
expect_any_instance_of(EmployeeImporter).to receive(:import_failed)
|
186
216
|
EmployeeImporter.sheet 5
|
187
|
-
|
217
|
+
expect {
|
218
|
+
EmployeeImporter.import('/dummy/file')
|
219
|
+
}.to raise_error
|
188
220
|
end
|
189
221
|
end
|
190
222
|
|
@@ -228,4 +260,84 @@ describe ActiveImporter::Base do
|
|
228
260
|
EmployeeImporter.import('/dummy/file')
|
229
261
|
end
|
230
262
|
end
|
263
|
+
|
264
|
+
describe '#initialize' do
|
265
|
+
context "when invoked with option 'transactional: true'" do
|
266
|
+
it 'declares the instance to be transactional even when the importer class is not' do
|
267
|
+
EmployeeImporter.transactional(false)
|
268
|
+
importer = EmployeeImporter.new('/dummy/file', transactional: true)
|
269
|
+
expect(importer).to be_transactional
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
context "when invoked with option 'transactional: false'" do
|
274
|
+
it 'does not override the class-wide setting' do
|
275
|
+
EmployeeImporter.transactional(true)
|
276
|
+
expect_any_instance_of(EmployeeImporter).to receive(:import_failed)
|
277
|
+
expect {
|
278
|
+
EmployeeImporter.new('/dummy/file', transactional: false)
|
279
|
+
}.to raise_error
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
describe '.transactional' do
|
285
|
+
let(:spreadsheet_data) { spreadsheet_data_with_errors }
|
286
|
+
|
287
|
+
before(:each) do
|
288
|
+
allow(EmployeeImporter).to receive(:new).once.and_return(importer)
|
289
|
+
end
|
290
|
+
|
291
|
+
context 'when called with true as an argument' do
|
292
|
+
before(:each) { EmployeeImporter.transactional(true) }
|
293
|
+
|
294
|
+
it 'declares all importers of its kind to be transactional' do
|
295
|
+
expect(EmployeeImporter).to be_transactional
|
296
|
+
importer = EmployeeImporter.new('/dummy/file')
|
297
|
+
expect(importer).to be_transactional
|
298
|
+
end
|
299
|
+
|
300
|
+
it 'runs the import process within a transaction' do
|
301
|
+
expect {
|
302
|
+
EmployeeImporter.import('/dummy/file') rescue nil
|
303
|
+
}.not_to change(Employee, :count)
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'exposes the exception that aborted the transaction' do
|
307
|
+
expect {
|
308
|
+
EmployeeImporter.import('/dummy/file')
|
309
|
+
}.to raise_error
|
310
|
+
end
|
311
|
+
|
312
|
+
it 'still invokes the :row_error event' do
|
313
|
+
expect(importer).to receive(:row_error)
|
314
|
+
EmployeeImporter.import('/dummy/file') rescue nil
|
315
|
+
end
|
316
|
+
|
317
|
+
it 'still invokes the :import_finished event' do
|
318
|
+
expect(importer).to receive(:import_finished)
|
319
|
+
EmployeeImporter.import('/dummy/file') rescue nil
|
320
|
+
end
|
321
|
+
|
322
|
+
it 'invokes the :import_aborted event' do
|
323
|
+
expect(importer).to receive(:import_aborted)
|
324
|
+
EmployeeImporter.import('/dummy/file') rescue nil
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
context 'when called with false as an argument' do
|
329
|
+
it 'does not run the import process within a transactio' do
|
330
|
+
EmployeeImporter.transactional(false)
|
331
|
+
expect {
|
332
|
+
EmployeeImporter.import('/dummy/file')
|
333
|
+
}.to change(Employee, :count).by(2)
|
334
|
+
end
|
335
|
+
|
336
|
+
it 'declares all importers of its kind not to be transactional' do
|
337
|
+
expect(EmployeeImporter).not_to be_transactional
|
338
|
+
importer = EmployeeImporter.new('/dummy/file')
|
339
|
+
expect(importer).not_to be_transactional
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
231
343
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
require 'active_importer'
|
2
2
|
|
3
|
+
# Require files in spec/support
|
4
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
5
|
+
|
3
6
|
# This file was generated by the `rspec --init` command. Conventionally, all
|
4
7
|
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
5
8
|
# Require this file using `require "spec_helper"` to ensure that it is only
|
@@ -21,3 +24,5 @@ RSpec.configure do |config|
|
|
21
24
|
# --seed 1234
|
22
25
|
config.order = 'random'
|
23
26
|
end
|
27
|
+
|
28
|
+
I18n.enforce_available_locales = false
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
|
5
|
+
ActiveRecord::Base.logger = Logger.new('/dev/null')
|
6
|
+
ActiveRecord::Migration.verbose = false
|
7
|
+
|
8
|
+
ActiveRecord::Schema.define do
|
9
|
+
create_table :employees, :force => true do |t|
|
10
|
+
t.column :name, :string
|
11
|
+
t.column :birth_date, :string
|
12
|
+
t.column :department_id, :integer
|
13
|
+
t.column :unused_field, :string
|
14
|
+
t.column :created_at, :datetime
|
15
|
+
t.column :updated_at, :datetime
|
16
|
+
end
|
17
|
+
end
|
@@ -1,14 +1,3 @@
|
|
1
|
-
require 'stubs/data_model'
|
2
|
-
require 'stubs/spreadsheet'
|
3
|
-
|
4
|
-
class Employee < DataModel
|
5
|
-
attr_accessor :name, :birth_date, :department, :department_id
|
6
|
-
|
7
|
-
def validate
|
8
|
-
errors << 'Invalid name' if name == 'Invalid'
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
1
|
class EmployeeBaseImporter < ActiveImporter::Base
|
13
2
|
on(:import_finished) { base_import_finished }
|
14
3
|
|
@@ -24,6 +13,8 @@ class EmployeeImporter < EmployeeBaseImporter
|
|
24
13
|
column 'Name', :name
|
25
14
|
column 'Birth Date', :birth_date
|
26
15
|
column 'Manager'
|
16
|
+
column 'Unused', :unused_field, optional: true
|
17
|
+
column 'Extra', optional: true
|
27
18
|
column ' Department ', :department_id do |value|
|
28
19
|
find_department(value)
|
29
20
|
end
|
File without changes
|
metadata
CHANGED
@@ -1,78 +1,97 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_importer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
5
|
-
prerelease:
|
4
|
+
version: 0.2.5
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Ernesto Garcia
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2014-02-21 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: roo
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
|
-
- -
|
17
|
+
- - ">="
|
20
18
|
- !ruby/object:Gem::Version
|
21
19
|
version: '0'
|
22
20
|
type: :runtime
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
|
-
- -
|
24
|
+
- - ">="
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: '0'
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: bundler
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
|
-
- - ~>
|
31
|
+
- - "~>"
|
36
32
|
- !ruby/object:Gem::Version
|
37
33
|
version: '1.3'
|
38
34
|
type: :development
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
|
-
- - ~>
|
38
|
+
- - "~>"
|
44
39
|
- !ruby/object:Gem::Version
|
45
40
|
version: '1.3'
|
46
41
|
- !ruby/object:Gem::Dependency
|
47
42
|
name: rake
|
48
43
|
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
44
|
requirements:
|
51
|
-
- -
|
45
|
+
- - ">="
|
52
46
|
- !ruby/object:Gem::Version
|
53
47
|
version: '0'
|
54
48
|
type: :development
|
55
49
|
prerelease: false
|
56
50
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
51
|
requirements:
|
59
|
-
- -
|
52
|
+
- - ">="
|
60
53
|
- !ruby/object:Gem::Version
|
61
54
|
version: '0'
|
62
55
|
- !ruby/object:Gem::Dependency
|
63
56
|
name: rspec
|
64
57
|
requirement: !ruby/object:Gem::Requirement
|
65
|
-
none: false
|
66
58
|
requirements:
|
67
|
-
- -
|
59
|
+
- - ">="
|
68
60
|
- !ruby/object:Gem::Version
|
69
61
|
version: '0'
|
70
62
|
type: :development
|
71
63
|
prerelease: false
|
72
64
|
version_requirements: !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
65
|
requirements:
|
75
|
-
- -
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sqlite3
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: activerecord
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
76
95
|
- !ruby/object:Gem::Version
|
77
96
|
version: '0'
|
78
97
|
description: Import tabular data from spreadsheets or similar sources into data models
|
@@ -82,8 +101,8 @@ executables: []
|
|
82
101
|
extensions: []
|
83
102
|
extra_rdoc_files: []
|
84
103
|
files:
|
85
|
-
- .gitignore
|
86
|
-
- .rspec
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rspec"
|
87
106
|
- Gemfile
|
88
107
|
- LICENSE.txt
|
89
108
|
- README.md
|
@@ -94,43 +113,38 @@ files:
|
|
94
113
|
- lib/active_importer/version.rb
|
95
114
|
- spec/active_importer/base_spec.rb
|
96
115
|
- spec/spec_helper.rb
|
97
|
-
- spec/
|
98
|
-
- spec/
|
99
|
-
- spec/
|
116
|
+
- spec/support/active_record/models.rb
|
117
|
+
- spec/support/active_record/schema.rb
|
118
|
+
- spec/support/employee_importer.rb
|
119
|
+
- spec/support/spreadsheet.rb
|
100
120
|
homepage: ''
|
101
121
|
licenses:
|
102
122
|
- MIT
|
123
|
+
metadata: {}
|
103
124
|
post_install_message:
|
104
125
|
rdoc_options: []
|
105
126
|
require_paths:
|
106
127
|
- lib
|
107
128
|
required_ruby_version: !ruby/object:Gem::Requirement
|
108
|
-
none: false
|
109
129
|
requirements:
|
110
|
-
- -
|
130
|
+
- - ">="
|
111
131
|
- !ruby/object:Gem::Version
|
112
132
|
version: '0'
|
113
|
-
segments:
|
114
|
-
- 0
|
115
|
-
hash: 3645783224165529099
|
116
133
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
117
|
-
none: false
|
118
134
|
requirements:
|
119
|
-
- -
|
135
|
+
- - ">="
|
120
136
|
- !ruby/object:Gem::Version
|
121
137
|
version: '0'
|
122
|
-
segments:
|
123
|
-
- 0
|
124
|
-
hash: 3645783224165529099
|
125
138
|
requirements: []
|
126
139
|
rubyforge_project:
|
127
|
-
rubygems_version:
|
140
|
+
rubygems_version: 2.2.0
|
128
141
|
signing_key:
|
129
|
-
specification_version:
|
142
|
+
specification_version: 4
|
130
143
|
summary: Import tabular data into data models
|
131
144
|
test_files:
|
132
145
|
- spec/active_importer/base_spec.rb
|
133
146
|
- spec/spec_helper.rb
|
134
|
-
- spec/
|
135
|
-
- spec/
|
136
|
-
- spec/
|
147
|
+
- spec/support/active_record/models.rb
|
148
|
+
- spec/support/active_record/schema.rb
|
149
|
+
- spec/support/employee_importer.rb
|
150
|
+
- spec/support/spreadsheet.rb
|
data/spec/stubs/data_model.rb
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
class DataModel
|
2
|
-
|
3
|
-
def self.count
|
4
|
-
@count ||= 0
|
5
|
-
end
|
6
|
-
|
7
|
-
attr_reader :errors
|
8
|
-
|
9
|
-
def initialize(attributes = {})
|
10
|
-
@new_record = true
|
11
|
-
@errors = []
|
12
|
-
attributes.each_pair do |key, value|
|
13
|
-
self[key] = value
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def []=(field, value)
|
18
|
-
send("#{field}=", value)
|
19
|
-
end
|
20
|
-
|
21
|
-
def to_s
|
22
|
-
"#{self.class.name}(#{attributes})"
|
23
|
-
end
|
24
|
-
|
25
|
-
def save
|
26
|
-
if valid?
|
27
|
-
self.class.send(:increment_count) if @new_record
|
28
|
-
@new_record = false
|
29
|
-
true
|
30
|
-
else
|
31
|
-
false
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def save!
|
36
|
-
raise 'Invalid model' unless save
|
37
|
-
end
|
38
|
-
|
39
|
-
def new_record?
|
40
|
-
@new_record
|
41
|
-
end
|
42
|
-
|
43
|
-
def valid?
|
44
|
-
validate
|
45
|
-
errors.empty?
|
46
|
-
end
|
47
|
-
|
48
|
-
def validate
|
49
|
-
# ...
|
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
|
62
|
-
end
|