active_importer 0.2.4 → 0.2.5
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.
- 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
|