act_as_importable 0.0.7 → 0.0.8
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/CHANGELOG.md +5 -0
- data/README.md +2 -0
- data/lib/act_as_importable/base.rb +23 -84
- data/lib/act_as_importable/config.rb +1 -0
- data/lib/act_as_importable/csv_importer.rb +18 -0
- data/lib/act_as_importable/importer.rb +150 -0
- data/lib/act_as_importable/version.rb +1 -1
- data/spec/act_as_importable/act_as_importable_spec.rb +33 -60
- data/spec/act_as_importable/importer_spec.rb +38 -0
- data/spec/spec_helper.rb +26 -0
- metadata +7 -2
data/CHANGELOG.md
ADDED
data/README.md
CHANGED
@@ -1,106 +1,45 @@
|
|
1
1
|
require 'active_support'
|
2
|
-
require '
|
2
|
+
require 'act_as_importable/importer'
|
3
|
+
require 'act_as_importable/csv_importer'
|
3
4
|
|
4
5
|
module ActAsImportable
|
5
6
|
module Base
|
6
7
|
extend ActiveSupport::Concern
|
7
8
|
|
9
|
+
included do
|
10
|
+
|
11
|
+
# This is used to store the data being import when there is an error
|
12
|
+
attr_accessor :import_data
|
13
|
+
|
14
|
+
end
|
15
|
+
|
8
16
|
module ClassMethods
|
9
17
|
|
10
18
|
def import_csv_file(file, options = {})
|
11
|
-
|
19
|
+
options.reverse_merge!(@default_import_options)
|
20
|
+
importer = ActAsImportable::CSVImporter.new(options)
|
21
|
+
importer.import_csv_file(file)
|
22
|
+
importer
|
12
23
|
end
|
13
24
|
|
14
25
|
def import_csv_text(text, options = {})
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
26
|
+
options.reverse_merge!(@default_import_options)
|
27
|
+
importer = ActAsImportable::CSVImporter.new(options)
|
28
|
+
importer.import_csv_text(text)
|
29
|
+
importer
|
19
30
|
end
|
20
31
|
|
21
32
|
def import_data(data, options = {})
|
22
|
-
|
23
|
-
|
24
|
-
|
33
|
+
options.reverse_merge!(@default_import_options)
|
34
|
+
importer = ActAsImportable::Importer.new(options)
|
35
|
+
importer.import_data(data)
|
36
|
+
importer
|
25
37
|
end
|
26
38
|
|
27
|
-
# Creates or updates a model record
|
28
|
-
# Existing records are found by the column(s) specified by the :uid option (default 'id').
|
29
|
-
# If the values for the uid columns are not provided the row will be ignored.
|
30
|
-
# If uid is set to nil it will import the row data as a new record.
|
31
39
|
def import_record(row, options = {})
|
32
40
|
options.reverse_merge!(@default_import_options)
|
33
|
-
|
34
|
-
|
35
|
-
convert_key_paths_to_values!(row)
|
36
|
-
row = filter_columns(row, options)
|
37
|
-
record = find_or_create_by_uids(uid_values(row, options))
|
38
|
-
remove_uid_values_from_row(row, options)
|
39
|
-
record.update_attributes(row)
|
40
|
-
unless record.save
|
41
|
-
Rails.logger.error(record.errors.full_messages)
|
42
|
-
end
|
43
|
-
record
|
44
|
-
end
|
45
|
-
|
46
|
-
def filter_columns(row, options = {})
|
47
|
-
except = Array(options[:except]).map { |i| i.to_s }
|
48
|
-
only = Array(options[:only]).map { |i| i.to_s }
|
49
|
-
row = row.reject { |key, value| except.include? key.to_s } if except.present?
|
50
|
-
row = row.select { |key, value| only.include? key.to_s } if only.present?
|
51
|
-
row
|
52
|
-
end
|
53
|
-
|
54
|
-
def uid_values(row, options)
|
55
|
-
Hash[Array(options[:uid]).map { |k| [k, row[k.to_sym]] }]
|
56
|
-
end
|
57
|
-
|
58
|
-
def remove_uid_values_from_row(row, options = {})
|
59
|
-
Array(options[:uid]).each do |field|
|
60
|
-
row.delete(field)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def find_association_value_with_attribute(name, attribute)
|
65
|
-
association = self.reflect_on_association(name.to_sym)
|
66
|
-
association.klass.where(attribute).first
|
67
|
-
end
|
68
|
-
|
69
|
-
def find_or_create_by_uids(attributes, &block)
|
70
|
-
find_by_uids(attributes) || create(attributes, &block)
|
71
|
-
end
|
72
|
-
|
73
|
-
def find_by_uids(attributes)
|
74
|
-
attributes.inject(self.scoped.readonly(false)) { |scope, key_value|
|
75
|
-
add_scope_for_field(scope, key_value[0].to_s, key_value[1])
|
76
|
-
}.first
|
77
|
-
end
|
78
|
-
|
79
|
-
def add_scope_for_field(scope, field, value)
|
80
|
-
return scope unless value
|
81
|
-
if (association = self.reflect_on_association(field.to_sym))
|
82
|
-
field = association.foreign_key
|
83
|
-
value = value.id
|
84
|
-
end
|
85
|
-
scope.where("#{self.table_name}.#{field} = ?", value)
|
86
|
-
end
|
87
|
-
|
88
|
-
# TODO: update this to support finding the association value with multiple columns
|
89
|
-
def convert_key_paths_to_values!(row)
|
90
|
-
key_path_attributes = row.select { |k,v| k.to_s.include? '.' }
|
91
|
-
key_path_attributes.each do |key, value|
|
92
|
-
association_name, uid_field = key.to_s.split('.')
|
93
|
-
row[association_name.to_sym] = find_association_value_with_attribute(association_name, uid_field => value)
|
94
|
-
row.delete(key.to_sym)
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
def read_file(file, options = {})
|
99
|
-
if options[:encoding]
|
100
|
-
File.read(file, :encoding => options[:encoding])
|
101
|
-
else
|
102
|
-
File.read(file)
|
103
|
-
end
|
41
|
+
importer = ActAsImportable::Importer.new(options)
|
42
|
+
importer.import_record(row)
|
104
43
|
end
|
105
44
|
|
106
45
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module ActAsImportable
|
4
|
+
class CSVImporter < ActAsImportable::Importer
|
5
|
+
|
6
|
+
def import_csv_file(file)
|
7
|
+
import_csv_text(File.read(file, options))
|
8
|
+
end
|
9
|
+
|
10
|
+
def import_csv_text(text)
|
11
|
+
csv = ::CSV.parse(text, :headers => options[:headers] || true)
|
12
|
+
csv.each do |row|
|
13
|
+
import_record(row.to_hash)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module ActAsImportable
|
2
|
+
class Importer
|
3
|
+
|
4
|
+
def initialize(options = {})
|
5
|
+
@default_options = options
|
6
|
+
@imported_records = []
|
7
|
+
@errors = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def options
|
11
|
+
@default_options
|
12
|
+
end
|
13
|
+
|
14
|
+
def import_data(data)
|
15
|
+
data.map do |row|
|
16
|
+
import_record(row)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def import_record(row)
|
21
|
+
row = prepare_row_for_import(row)
|
22
|
+
record = find_or_create_record(row)
|
23
|
+
record.update_attributes(row)
|
24
|
+
record.save
|
25
|
+
imported_records << record
|
26
|
+
record
|
27
|
+
rescue Exception => e
|
28
|
+
record = model_class.new
|
29
|
+
# Assign the valid attributes (without saving)
|
30
|
+
record.assign_attributes(row.select { |k,v| record.attributes.keys.include? k })
|
31
|
+
record.errors.add :base, e.message
|
32
|
+
record.import_data = row if record.respond_to? :import_data=
|
33
|
+
imported_records << record
|
34
|
+
record
|
35
|
+
end
|
36
|
+
|
37
|
+
def missing_uid_values(row)
|
38
|
+
uid_values(row).select { |k, v| v.blank? }
|
39
|
+
end
|
40
|
+
|
41
|
+
def imported_records
|
42
|
+
@imported_records ||= []
|
43
|
+
end
|
44
|
+
|
45
|
+
def successful_imports
|
46
|
+
# Note: We don't want to re-validate the objects.
|
47
|
+
imported_records.select { |r| r.errors.empty? }
|
48
|
+
end
|
49
|
+
|
50
|
+
def failed_imports
|
51
|
+
# Note: We don't want to re-validate the objects.
|
52
|
+
imported_records.reject { |r| r.errors.empty? }
|
53
|
+
end
|
54
|
+
|
55
|
+
def model_class
|
56
|
+
options[:model_class]
|
57
|
+
end
|
58
|
+
|
59
|
+
def filter_columns(row)
|
60
|
+
except = Array(options[:except]).map { |i| i.to_s }
|
61
|
+
only = Array(options[:only]).map { |i| i.to_s }
|
62
|
+
row.reject! { |key, value| except.include? key.to_s } if except.present?
|
63
|
+
row.select! { |key, value| only.include? key.to_s } if only.present?
|
64
|
+
row
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def prepare_row_for_import(row)
|
70
|
+
row = row.with_indifferent_access
|
71
|
+
add_default_values_to_row(row)
|
72
|
+
convert_key_paths_to_values(row)
|
73
|
+
filter_columns(row)
|
74
|
+
row
|
75
|
+
end
|
76
|
+
|
77
|
+
def add_default_values_to_row(row)
|
78
|
+
row.reverse_merge!(options[:default_values]) if options[:default_values]
|
79
|
+
end
|
80
|
+
|
81
|
+
def uid_keys
|
82
|
+
Array(options[:uid])
|
83
|
+
end
|
84
|
+
|
85
|
+
def uid_values(row)
|
86
|
+
uid_keys.each_with_object({}) do |key, result|
|
87
|
+
result[key] = row[key.to_sym]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def remove_uid_values_from_row(row, options = {})
|
92
|
+
Array(options[:uid]).each do |field|
|
93
|
+
row.delete(field)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def find_association_value_with_attribute(name, attribute)
|
98
|
+
association = model_class.reflect_on_association(name.to_sym)
|
99
|
+
association.klass.where(attribute).first
|
100
|
+
end
|
101
|
+
|
102
|
+
def find_or_create_record(row)
|
103
|
+
if missing_uid_values(row).present?
|
104
|
+
raise "Missing the following uids attributes. #{missing_uid_values(row).keys}"
|
105
|
+
end
|
106
|
+
|
107
|
+
record = find_or_create_by_uids(uid_values(row))
|
108
|
+
remove_uid_values_from_row(row)
|
109
|
+
record
|
110
|
+
end
|
111
|
+
|
112
|
+
def find_or_create_by_uids(attributes, &block)
|
113
|
+
find_by_uids(attributes) || model_class.create(attributes, &block)
|
114
|
+
end
|
115
|
+
|
116
|
+
def find_by_uids(attributes)
|
117
|
+
results = attributes.inject(model_class.scoped.readonly(false)) { |scope, key_value|
|
118
|
+
add_scope_for_field(scope, key_value[0].to_s, key_value[1])
|
119
|
+
}
|
120
|
+
if results.count > 1
|
121
|
+
raise "Multiple records found with uid attributes. Attributes: #{attributes}"
|
122
|
+
end
|
123
|
+
results.first
|
124
|
+
end
|
125
|
+
|
126
|
+
def add_scope_for_field(scope, field, value)
|
127
|
+
return scope unless value
|
128
|
+
if (association = model_class.reflect_on_association(field.to_sym))
|
129
|
+
field = association.foreign_key
|
130
|
+
value = value.id
|
131
|
+
end
|
132
|
+
scope.where("#{model_class.table_name}.#{field} = ?", value)
|
133
|
+
end
|
134
|
+
|
135
|
+
# TODO: update this to support finding the association value with multiple columns
|
136
|
+
def convert_key_paths_to_values(row)
|
137
|
+
key_path_attributes = row.select { |k, v| k.to_s.include? '.' }
|
138
|
+
key_path_attributes.each do |key, value|
|
139
|
+
association_name, uid_field = key.to_s.split('.')
|
140
|
+
association_value = find_association_value_with_attribute(association_name, uid_field => value)
|
141
|
+
if association_value.blank?
|
142
|
+
raise "Failed to find #{association_name} with #{uid_field} = #{value}"
|
143
|
+
end
|
144
|
+
row[association_name.to_sym] = association_value
|
145
|
+
row.delete(key.to_sym)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
@@ -1,31 +1,5 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
2
|
|
3
|
-
# create the tables for the tests
|
4
|
-
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS 'categories'")
|
5
|
-
ActiveRecord::Base.connection.create_table(:categories) do |t|
|
6
|
-
t.string :name
|
7
|
-
end
|
8
|
-
|
9
|
-
class Category < ActiveRecord::Base
|
10
|
-
has_many :items
|
11
|
-
end
|
12
|
-
|
13
|
-
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS 'items'")
|
14
|
-
ActiveRecord::Base.connection.create_table(:items) do |t|
|
15
|
-
t.integer :category_id
|
16
|
-
t.string :name
|
17
|
-
t.float :price
|
18
|
-
end
|
19
|
-
|
20
|
-
class Item < ActiveRecord::Base
|
21
|
-
# This line isn't needed in a real Rails app.
|
22
|
-
include ActAsImportable::Config
|
23
|
-
|
24
|
-
act_as_importable :uid => 'name'
|
25
|
-
|
26
|
-
belongs_to :category
|
27
|
-
end
|
28
|
-
|
29
3
|
describe "an act_as_importable model" do
|
30
4
|
|
31
5
|
before(:each) do
|
@@ -45,7 +19,7 @@ describe "an act_as_importable model" do
|
|
45
19
|
it { should respond_to :import_record }
|
46
20
|
it { should respond_to :default_import_options }
|
47
21
|
|
48
|
-
let(:default_options) { {:uid => 'name'} }
|
22
|
+
let(:default_options) { {:uid => 'name', :model_class => Item} }
|
49
23
|
|
50
24
|
it "should have the correct default import options" do
|
51
25
|
Item.default_import_options.should == default_options
|
@@ -54,11 +28,11 @@ describe "an act_as_importable model" do
|
|
54
28
|
describe "import csv file" do
|
55
29
|
let(:file) { 'spec/fixtures/items.csv' }
|
56
30
|
it 'should call import_text with contents of file' do
|
57
|
-
|
31
|
+
ActAsImportable::CSVImporter.any_instance.should_receive(:import_csv_text).with(File.read(file))
|
58
32
|
Item.import_csv_file(file)
|
59
33
|
end
|
60
|
-
it "should return an
|
61
|
-
Item.import_csv_file(file).should
|
34
|
+
it "should return an importer instance" do
|
35
|
+
Item.import_csv_file(file).should be_a ActAsImportable::Importer
|
62
36
|
end
|
63
37
|
end
|
64
38
|
|
@@ -66,13 +40,23 @@ describe "an act_as_importable model" do
|
|
66
40
|
let(:text) { "name,price\nBeer,2.5\nApple,0.5" }
|
67
41
|
|
68
42
|
it 'should call import_record with row hashes' do
|
69
|
-
|
70
|
-
|
43
|
+
ActAsImportable::CSVImporter.any_instance.should_receive(:import_record).with({'name' => 'Beer', 'price' => '2.5'}).once
|
44
|
+
ActAsImportable::CSVImporter.any_instance.should_receive(:import_record).with({'name' => 'Apple', 'price' => '0.5'}).once
|
71
45
|
Item.import_csv_text(text)
|
72
46
|
end
|
73
47
|
|
74
|
-
it "should return an
|
75
|
-
Item.import_csv_text(text).should
|
48
|
+
it "should return an importer instance" do
|
49
|
+
Item.import_csv_text(text).should be_a ActAsImportable::Importer
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should import 2 records successfully" do
|
53
|
+
result = Item.import_csv_text(text)
|
54
|
+
result.successful_imports.count.should == 2
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should have no errors" do
|
58
|
+
result = Item.import_csv_text(text)
|
59
|
+
result.failed_imports.count.should == 0
|
76
60
|
end
|
77
61
|
end
|
78
62
|
|
@@ -82,13 +66,23 @@ describe "an act_as_importable model" do
|
|
82
66
|
let(:data) { [beer, apple] }
|
83
67
|
|
84
68
|
it 'should call import_record with row hashes' do
|
85
|
-
|
86
|
-
|
69
|
+
ActAsImportable::Importer.any_instance.should_receive(:import_record).with(beer).once
|
70
|
+
ActAsImportable::Importer.any_instance.should_receive(:import_record).with(apple).once
|
87
71
|
Item.import_data(data)
|
88
72
|
end
|
89
73
|
|
90
|
-
it "should return an
|
91
|
-
Item.import_data(data).should
|
74
|
+
it "should return an importer instance" do
|
75
|
+
Item.import_data(data).should be_a ActAsImportable::Importer
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should import 2 records successfully" do
|
79
|
+
result = Item.import_data(data)
|
80
|
+
result.successful_imports.count.should == 2
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should have no errors" do
|
84
|
+
result = Item.import_data(data)
|
85
|
+
result.failed_imports.count.should == 0
|
92
86
|
end
|
93
87
|
end
|
94
88
|
|
@@ -113,7 +107,7 @@ describe "an act_as_importable model" do
|
|
113
107
|
Item.import_record(row, :uid => :name)
|
114
108
|
@existing_item.reload.price.should == 2.5
|
115
109
|
end
|
116
|
-
it "should not create a new
|
110
|
+
it "should not create a new record" do
|
117
111
|
expect { Item.import_record(row, :uid => 'name') }.to change { Item.count }.by(0)
|
118
112
|
end
|
119
113
|
end
|
@@ -208,26 +202,5 @@ describe "an act_as_importable model" do
|
|
208
202
|
|
209
203
|
end
|
210
204
|
|
211
|
-
describe "#filter_columns" do
|
212
|
-
let(:row) { {:name => 'Beer', :price => 2.5}.with_indifferent_access }
|
213
|
-
|
214
|
-
it 'should filter columns when importing each record' do
|
215
|
-
Item.should_receive(:filter_columns).with(row, default_options).and_return(row)
|
216
|
-
Item.import_record(row)
|
217
|
-
end
|
218
|
-
|
219
|
-
it "should not modify row if no options provided" do
|
220
|
-
Item.filter_columns(row).should == row
|
221
|
-
end
|
222
|
-
|
223
|
-
it "should remove columns specified by the :except option" do
|
224
|
-
Item.filter_columns(row, :except => :price).should == {'name' => 'Beer'}
|
225
|
-
end
|
226
|
-
|
227
|
-
it "should remove columns not specified by the :only option" do
|
228
|
-
Item.filter_columns(row, :only => :price).should == {'price' => 2.5}
|
229
|
-
end
|
230
|
-
end
|
231
|
-
|
232
205
|
end
|
233
206
|
|
@@ -0,0 +1,38 @@
|
|
1
|
+
describe ActAsImportable::Importer do
|
2
|
+
|
3
|
+
before(:each) do
|
4
|
+
ActiveRecord::Base.connection.increment_open_transactions
|
5
|
+
ActiveRecord::Base.connection.begin_db_transaction
|
6
|
+
end
|
7
|
+
|
8
|
+
after(:each) do
|
9
|
+
ActiveRecord::Base.connection.rollback_db_transaction
|
10
|
+
ActiveRecord::Base.connection.decrement_open_transactions
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#filter_columns" do
|
14
|
+
let(:row) { {:name => 'Beer', :price => 2.5}.with_indifferent_access }
|
15
|
+
|
16
|
+
it 'should filter columns when importing each record' do
|
17
|
+
importer = ActAsImportable::Importer.new(:uid => :name, :model_class => Item)
|
18
|
+
importer.should_receive(:filter_columns).with(row).and_return(row)
|
19
|
+
importer.import_record(row)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should not modify row if no options provided" do
|
23
|
+
importer = ActAsImportable::Importer.new
|
24
|
+
importer.filter_columns(row).should == row
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should remove columns specified by the :except option" do
|
28
|
+
importer = ActAsImportable::Importer.new(:except => :price)
|
29
|
+
importer.filter_columns(row).should == {'name' => 'Beer'}
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should remove columns not specified by the :only option" do
|
33
|
+
importer = ActAsImportable::Importer.new(:only => :price)
|
34
|
+
importer.filter_columns(row).should == {'price' => 2.5}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -18,4 +18,30 @@ ActiveRecord::Base.establish_connection(
|
|
18
18
|
)
|
19
19
|
|
20
20
|
RSpec.configure do |config|
|
21
|
+
end
|
22
|
+
|
23
|
+
# create the tables for the tests
|
24
|
+
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS 'categories'")
|
25
|
+
ActiveRecord::Base.connection.create_table(:categories) do |t|
|
26
|
+
t.string :name
|
27
|
+
end
|
28
|
+
|
29
|
+
class Category < ActiveRecord::Base
|
30
|
+
has_many :items
|
31
|
+
end
|
32
|
+
|
33
|
+
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS 'items'")
|
34
|
+
ActiveRecord::Base.connection.create_table(:items) do |t|
|
35
|
+
t.integer :category_id
|
36
|
+
t.string :name
|
37
|
+
t.float :price
|
38
|
+
end
|
39
|
+
|
40
|
+
class Item < ActiveRecord::Base
|
41
|
+
# This line isn't needed in a real Rails app.
|
42
|
+
include ActAsImportable::Config
|
43
|
+
|
44
|
+
act_as_importable :uid => 'name'
|
45
|
+
|
46
|
+
belongs_to :category
|
21
47
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: act_as_importable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
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-
|
12
|
+
date: 2013-04-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -84,6 +84,7 @@ extra_rdoc_files: []
|
|
84
84
|
files:
|
85
85
|
- .gitignore
|
86
86
|
- .rvmrc
|
87
|
+
- CHANGELOG.md
|
87
88
|
- Gemfile
|
88
89
|
- LICENSE.txt
|
89
90
|
- README.md
|
@@ -93,9 +94,12 @@ files:
|
|
93
94
|
- lib/act_as_importable.rb
|
94
95
|
- lib/act_as_importable/base.rb
|
95
96
|
- lib/act_as_importable/config.rb
|
97
|
+
- lib/act_as_importable/csv_importer.rb
|
98
|
+
- lib/act_as_importable/importer.rb
|
96
99
|
- lib/act_as_importable/railtie.rb
|
97
100
|
- lib/act_as_importable/version.rb
|
98
101
|
- spec/act_as_importable/act_as_importable_spec.rb
|
102
|
+
- spec/act_as_importable/importer_spec.rb
|
99
103
|
- spec/fixtures/items.csv
|
100
104
|
- spec/spec_helper.rb
|
101
105
|
homepage: https://github.com/Col/act_as_importable
|
@@ -124,5 +128,6 @@ specification_version: 3
|
|
124
128
|
summary: Helps import records from CSV files.
|
125
129
|
test_files:
|
126
130
|
- spec/act_as_importable/act_as_importable_spec.rb
|
131
|
+
- spec/act_as_importable/importer_spec.rb
|
127
132
|
- spec/fixtures/items.csv
|
128
133
|
- spec/spec_helper.rb
|