active_importer 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -80,12 +80,16 @@ class EmployeeImporter < ActiveImporter::Base
80
80
 
81
81
  attr_reader :row_count
82
82
 
83
- column 'First name', :first_name
84
- column 'Last name', :last_name
83
+ column 'First name'
84
+ column 'Last name'
85
85
  column 'Department', :department do |department_name|
86
86
  Department.find_by(name: department_name)
87
87
  end
88
88
 
89
+ on :row_processing do
90
+ model.full_name = [row['First name'], row['Last name']].join(' ')
91
+ end
92
+
89
93
  on :import_started do
90
94
  @row_count = 0
91
95
  end
@@ -117,6 +121,8 @@ The supported events are:
117
121
  event is fired by an importer, none of its other events are ever fired.
118
122
  - **import_started:** Fired once at the beginning of the data processing,
119
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.
120
126
  - **row_processed:** Fired once for each row that has been processed,
121
127
  regardless of whether it resulted in success or error.
122
128
  - **row_success:** Fired once for each row that was imported successfully into
@@ -132,6 +138,37 @@ all the importer attributes and instance variables. Error-related events
132
138
  (`:import_failed` and `:row_error`) pass to the blocks the instance of the
133
139
  exception that provoked the error condition.
134
140
 
141
+ Additionally, all the `row_*` events have access to the `row` and `model`
142
+ variables, which reference the spreadsheet row being processed, and the model
143
+ object where the row data is being stored, respectively. This feature is
144
+ specifically useful for the `:row_processing` event handler, which is triggered
145
+ while a row is being processed, and before the corresponding data model is
146
+ saved. This allows to define any complex data-import logic that cannot be
147
+ expressed in terms of mapping a column to a data field.
148
+
149
+ ### Selecting the model instance to import into
150
+
151
+ By default, the importer will attempt to generate a new model instance per row
152
+ processed. The importer can be instructed to update records instead, if they
153
+ already exist, instead of always attempting to generate a new one.
154
+
155
+ ```ruby
156
+ class EmployeeImporter
157
+ imports Employee
158
+
159
+ fetch_model do
160
+ Employee.where(first_name: row['First name'], last_name: row['Last name']).first_or_initialize
161
+ end
162
+
163
+ # ...
164
+ end
165
+ ```
166
+
167
+ The code above specifies that, for each row, the importer should attempt to
168
+ find an existing model for the employee with the first and last name in the row
169
+ being processed. If this record exist, the row data will be used to update the
170
+ given model instance. Otherwise, a new employee record will be created.
171
+
135
172
  ## Contributing
136
173
 
137
174
  1. Fork it
@@ -26,6 +26,14 @@ module ActiveImporter
26
26
  self.class.model_class
27
27
  end
28
28
 
29
+ def self.fetch_model(&block)
30
+ @fetch_model_block = block
31
+ end
32
+
33
+ def self.fetch_model_block
34
+ @fetch_model_block
35
+ end
36
+
29
37
  def self.column(title, field = nil, &block)
30
38
  title = title.strip
31
39
  if columns[title]
@@ -45,6 +53,7 @@ module ActiveImporter
45
53
  EVENTS = [
46
54
  :row_success,
47
55
  :row_error,
56
+ :row_processing,
48
57
  :row_processed,
49
58
  :import_started,
50
59
  :import_finished,
@@ -77,6 +86,7 @@ module ActiveImporter
77
86
 
78
87
  class << self
79
88
  private :fire_event
89
+ private :fetch_model_block
80
90
  end
81
91
 
82
92
  #
@@ -104,8 +114,16 @@ module ActiveImporter
104
114
  fire_event :import_failed, e
105
115
  end
106
116
 
117
+ def fetch_model_block
118
+ self.class.send(:fetch_model_block)
119
+ end
120
+
107
121
  def fetch_model
108
- model_class.new
122
+ if fetch_model_block
123
+ self.instance_exec(&fetch_model_block)
124
+ else
125
+ model_class.new
126
+ end
109
127
  end
110
128
 
111
129
  def import
@@ -131,9 +149,6 @@ module ActiveImporter
131
149
  row_errors.count
132
150
  end
133
151
 
134
- def hook
135
- end
136
-
137
152
  private
138
153
 
139
154
  def columns
@@ -142,7 +157,7 @@ module ActiveImporter
142
157
 
143
158
  def find_header_index
144
159
  (1..@book.last_row).each do |index|
145
- row = @book.row(index).map(&:strip)
160
+ row = @book.row(index).map { |cell| cell.to_s.strip }
146
161
  return index if columns.keys.all? { |item| row.include?(item) }
147
162
  end
148
163
  return nil
@@ -182,7 +197,7 @@ module ActiveImporter
182
197
  value = self.instance_exec(value, &transform) if transform
183
198
  model[field_name] = value
184
199
  end
185
- hook
200
+ fire_event :row_processing
186
201
  end
187
202
 
188
203
  def row_to_hash(row)
@@ -1,3 +1,3 @@
1
1
  module ActiveImporter
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -14,6 +14,7 @@ describe ActiveImporter::Base do
14
14
 
15
15
  before do
16
16
  expect(Roo::Spreadsheet).to receive(:open).and_return { Spreadsheet.new(spreadsheet_data) }
17
+ EmployeeImporter.instance_variable_set(:@fetch_model_block, nil)
17
18
  end
18
19
 
19
20
  it 'imports all data from the spreadsheet into the model' do
@@ -88,7 +89,7 @@ describe ActiveImporter::Base do
88
89
  let(:spreadsheet_data) do
89
90
  [
90
91
  [],
91
- ['List of employees', '', 'Company Name'],
92
+ ['List of employees', '', nil, 'Company Name'],
92
93
  ['Ordered by', 'Birth Date'],
93
94
  ['Name', 'Department', 'Birth Date', 'Manager'],
94
95
  ['John Doe', 'IT', '2013-10-25'],
@@ -102,19 +103,17 @@ describe ActiveImporter::Base do
102
103
  end
103
104
 
104
105
  describe '.fetch_model' do
105
- let(:model) { Employee.new }
106
-
107
106
  it 'controls what model instance is loaded for each given row' do
108
- expect(EmployeeImporter).to receive(:new).once.and_return(importer)
109
- expect(importer).to receive(:fetch_model).twice.and_return(model)
107
+ model = Employee.new
108
+ EmployeeImporter.fetch_model { model }
110
109
  expect { EmployeeImporter.import('/dummy/file') }.to change(Employee, :count).by(1)
111
110
  end
112
111
  end
113
112
 
114
- describe '.hook' do
113
+ describe 'row_processing event' do
115
114
  it 'allows the importer to modify the model for each row' do
116
115
  expect(EmployeeImporter).to receive(:new).once.and_return(importer)
117
- expect(importer).to receive(:hook).twice
116
+ expect(importer).to receive(:row_processing).twice
118
117
  EmployeeImporter.import('/dummy/file')
119
118
  end
120
119
  end
@@ -1,8 +1,7 @@
1
1
  class DataModel
2
- @@count = 0
3
2
 
4
3
  def self.count
5
- @@count
4
+ @count ||= 0
6
5
  end
7
6
 
8
7
  attr_reader :errors
@@ -25,7 +24,7 @@ class DataModel
25
24
 
26
25
  def save
27
26
  if valid?
28
- @@count += 1 if @new_record
27
+ self.class.send(:increment_count) if @new_record
29
28
  @new_record = false
30
29
  true
31
30
  else
@@ -43,10 +42,21 @@ class DataModel
43
42
 
44
43
  def valid?
45
44
  validate
46
- @errors.empty?
45
+ errors.empty?
47
46
  end
48
47
 
49
48
  def validate
50
49
  # ...
51
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
52
62
  end
@@ -5,7 +5,7 @@ class Employee < DataModel
5
5
  attr_accessor :name, :birth_date, :department, :department_id
6
6
 
7
7
  def validate
8
- @errors << 'Invalid name' if name == 'Invalid'
8
+ errors << 'Invalid name' if name == 'Invalid'
9
9
  end
10
10
  end
11
11
 
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.1.1
4
+ version: 0.2.0
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-08 00:00:00.000000000 Z
12
+ date: 2013-11-11 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: -839045621640734141
115
+ hash: -1076262899309802271
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: -839045621640734141
124
+ hash: -1076262899309802271
125
125
  requirements: []
126
126
  rubyforge_project:
127
127
  rubygems_version: 1.8.23