active_importer 0.1.1 → 0.2.0

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 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