act_as_importable 0.0.1
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/.gitignore +17 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +39 -0
- data/Rakefile +6 -0
- data/act_as_importable.gemspec +25 -0
- data/lib/act_as_importable.rb +10 -0
- data/lib/act_as_importable/base.rb +95 -0
- data/lib/act_as_importable/config.rb +11 -0
- data/lib/act_as_importable/version.rb +3 -0
- data/spec/act_as_importable/act_as_importable_spec.rb +115 -0
- data/spec/fixtures/items.csv +3 -0
- data/spec/spec_helper.rb +20 -0
- metadata +125 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Colin Harris
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# ActAsImportable
|
2
|
+
|
3
|
+
Help you easily import models from a CSV file.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'act_as_importable'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install act_as_importable
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
class User < ActiveRecord::Base
|
23
|
+
act_as_importable
|
24
|
+
end
|
25
|
+
```
|
26
|
+
|
27
|
+
## Test
|
28
|
+
|
29
|
+
```shell
|
30
|
+
rake
|
31
|
+
```
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
1. Fork it
|
36
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
37
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
38
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
39
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'act_as_importable/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "act_as_importable"
|
8
|
+
gem.version = ActAsImportable::VERSION
|
9
|
+
gem.authors = ["Colin Harris"]
|
10
|
+
gem.email = ["col.w.harris@gmail.com"]
|
11
|
+
gem.description = %q{Helps import models from CSV files.}
|
12
|
+
gem.summary = %q{Helps import models from CSV files.}
|
13
|
+
gem.homepage = "https://github.com/Col/act_as_importable"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency 'activesupport', '~> 3.2.11'
|
21
|
+
gem.add_dependency 'activerecord', '~> 3.2.11'
|
22
|
+
|
23
|
+
gem.add_development_dependency "rspec"
|
24
|
+
gem.add_development_dependency "sqlite3"
|
25
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'act_as_importable/base'
|
2
|
+
require 'act_as_importable/config'
|
3
|
+
require 'csv'
|
4
|
+
|
5
|
+
module ActAsImportable
|
6
|
+
include ActAsImportable::Base
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'act_as_importable/railtie.rb' if defined?(Rails)
|
10
|
+
ActiveRecord::Base.send :include, ActAsImportable::Config
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module ActAsImportable::Base
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
|
8
|
+
##
|
9
|
+
# Imports a data file into a model
|
10
|
+
# Existing records are found by the :uid.
|
11
|
+
# This can be changed by providing the :unique_identifier option or overriding the default_unique_identifier class method.
|
12
|
+
def import_file(file, options = {})
|
13
|
+
import_text(File.read(file), options)
|
14
|
+
end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Imports csv text into a model
|
18
|
+
def import_text(text, options = {})
|
19
|
+
csv = ::CSV.parse(text, :headers => true)
|
20
|
+
csv.each do |row|
|
21
|
+
row = row.to_hash.with_indifferent_access
|
22
|
+
import_record(row, options)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def import_record(row, options = {})
|
27
|
+
row = filter_columns(row, options)
|
28
|
+
record = find_existing_record(row, options)
|
29
|
+
remove_unique_identifiers(row, options) if record
|
30
|
+
record ||= self.new()
|
31
|
+
update_associations(record, row)
|
32
|
+
record.update_attributes(row)
|
33
|
+
record.save!
|
34
|
+
end
|
35
|
+
|
36
|
+
def filter_columns(row, options = {})
|
37
|
+
row = row.reject { |key, value| Array(options[:except]).include? key } if options[:except]
|
38
|
+
row = row.select { |key, value| Array(options[:only]).include? key } if options[:only]
|
39
|
+
row
|
40
|
+
end
|
41
|
+
|
42
|
+
def remove_unique_identifiers(row, options = {})
|
43
|
+
Array(options[:unique_identifier]).each do |field|
|
44
|
+
row.delete(field)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Updates any associations specified in the import data.
|
50
|
+
# Associations are specified in the header by separating the association and the field by a '.'
|
51
|
+
def update_associations(existing, row)
|
52
|
+
row.each_key do |key|
|
53
|
+
key = key.to_s
|
54
|
+
if key.include?('.')
|
55
|
+
association_name = key.split('.').first
|
56
|
+
field = key.split('.').last
|
57
|
+
association = self.reflect_on_association(association_name.to_sym)
|
58
|
+
association_value = association.klass.where("#{field} = ?", row[key]).first
|
59
|
+
existing.send("#{association_name}=", association_value) if association_value
|
60
|
+
row.delete(key)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Fetches the existing record based on the identifier field(s) specified by the :unique_identifier option.
|
67
|
+
# Defaults to the field specified by 'default_unique_identifier'
|
68
|
+
def find_existing_record(row, options = {})
|
69
|
+
return unless options[:unique_identifier]
|
70
|
+
fields = Array(options[:unique_identifier])
|
71
|
+
fields.inject(self.scoped.readonly(false)) { |scope, field|
|
72
|
+
add_scope_for_field(scope, field.to_s, row)
|
73
|
+
}.first
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# Refines an existing scope to include a new field
|
78
|
+
# The field can traverse 'belongs_to' associations by joining the association and field with a '.'
|
79
|
+
# The query value should be found within the hash with the field as the key.
|
80
|
+
# Notes: currently only supports field paths through one association.
|
81
|
+
def add_scope_for_field(scope, field, hash)
|
82
|
+
value = hash[field]
|
83
|
+
return scope unless value
|
84
|
+
if field.include?('.')
|
85
|
+
association = field.split('.').first
|
86
|
+
field_name = field.split('.').last
|
87
|
+
scope.joins(association.to_sym).where("#{association.pluralize}.#{field_name} = ?", value)
|
88
|
+
else
|
89
|
+
scope.where("#{field} = ?", value)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
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
|
+
act_as_importable
|
22
|
+
|
23
|
+
belongs_to :category
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "an act_as_importable model" do
|
27
|
+
|
28
|
+
before(:each) do
|
29
|
+
ActiveRecord::Base.connection.increment_open_transactions
|
30
|
+
ActiveRecord::Base.connection.begin_db_transaction
|
31
|
+
end
|
32
|
+
|
33
|
+
after(:each) do
|
34
|
+
ActiveRecord::Base.connection.rollback_db_transaction
|
35
|
+
ActiveRecord::Base.connection.decrement_open_transactions
|
36
|
+
end
|
37
|
+
|
38
|
+
subject { Item }
|
39
|
+
|
40
|
+
it { should respond_to :import_file }
|
41
|
+
it { should respond_to :import_text }
|
42
|
+
it { should respond_to :import_record }
|
43
|
+
|
44
|
+
describe "import csv file" do
|
45
|
+
let(:file) { 'spec/fixtures/items.csv' }
|
46
|
+
it 'should call import_text with context of file' do
|
47
|
+
Item.should_receive(:import_text).with(File.read(file), {})
|
48
|
+
Item.import_file(file)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "import csv text" do
|
53
|
+
let(:text) { "name,price\nBeer,2.5\nApple,0.5" }
|
54
|
+
it 'should call import_record with row hashes' do
|
55
|
+
Item.should_receive(:import_record).with({'name' => 'Beer', 'price' => '2.5'}, {}).once
|
56
|
+
Item.should_receive(:import_record).with({'name' => 'Apple', 'price' => '0.5'}, {}).once
|
57
|
+
Item.import_text(text)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "import record" do
|
62
|
+
let(:row) { {:name => 'Beer', :price => 2.5} }
|
63
|
+
|
64
|
+
it 'should import an item' do
|
65
|
+
expect { Item.import_record(row) }.to change{Item.count}.by(1)
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "unique identifier" do
|
69
|
+
before :each do
|
70
|
+
@existing_item = Item.create!(:name => 'Beer', :price => 1.0)
|
71
|
+
end
|
72
|
+
it "should update an existing record with matching unique identifier" do
|
73
|
+
Item.import_record(row, :unique_identifier => 'name')
|
74
|
+
@existing_item.reload.price.should == 2.5
|
75
|
+
end
|
76
|
+
it "should not create a new item" do
|
77
|
+
expect { Item.import_record(row, :unique_identifier => 'name') }.to change { Item.count }.by(0)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "import record with association" do
|
83
|
+
let(:row) { {:name => 'Beer', :price => 2.5, 'category.name' => 'Beverage'} }
|
84
|
+
before :each do
|
85
|
+
@category = Category.create!(:name => 'Beverage')
|
86
|
+
end
|
87
|
+
it "should import item with a category" do
|
88
|
+
Item.import_record(row)
|
89
|
+
Item.first.category.should == @category
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "#filter_columns" do
|
94
|
+
let(:row) { {:name => 'Beer', :price => 2.5} }
|
95
|
+
|
96
|
+
it 'should filter columns when importing each record' do
|
97
|
+
Item.should_receive(:filter_columns).with(row, {}).and_return(row)
|
98
|
+
Item.import_record(row)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should not modify row if no options provided" do
|
102
|
+
Item.filter_columns(row).should == {:name => 'Beer', :price => 2.5}
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should remove columns specified by the :except option" do
|
106
|
+
Item.filter_columns(row, :except => :price).should == {:name => 'Beer'}
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should remove columns not specified by the :only option" do
|
110
|
+
Item.filter_columns(row, :only => :price).should == {:price => 2.5}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'bundler/setup'
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
|
8
|
+
require 'active_record'
|
9
|
+
require 'active_support'
|
10
|
+
require 'act_as_importable'
|
11
|
+
require 'rspec'
|
12
|
+
|
13
|
+
root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
14
|
+
ActiveRecord::Base.establish_connection(
|
15
|
+
:adapter => "sqlite3",
|
16
|
+
:database => "#{root}/db/act_as_importable.db"
|
17
|
+
)
|
18
|
+
|
19
|
+
RSpec.configure do |config|
|
20
|
+
end
|
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: act_as_importable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Colin Harris
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-03-05 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.11
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.2.11
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: activerecord
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 3.2.11
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 3.2.11
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: sqlite3
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description: Helps import models from CSV files.
|
79
|
+
email:
|
80
|
+
- col.w.harris@gmail.com
|
81
|
+
executables: []
|
82
|
+
extensions: []
|
83
|
+
extra_rdoc_files: []
|
84
|
+
files:
|
85
|
+
- .gitignore
|
86
|
+
- Gemfile
|
87
|
+
- LICENSE.txt
|
88
|
+
- README.md
|
89
|
+
- Rakefile
|
90
|
+
- act_as_importable.gemspec
|
91
|
+
- lib/act_as_importable.rb
|
92
|
+
- lib/act_as_importable/base.rb
|
93
|
+
- lib/act_as_importable/config.rb
|
94
|
+
- lib/act_as_importable/version.rb
|
95
|
+
- spec/act_as_importable/act_as_importable_spec.rb
|
96
|
+
- spec/fixtures/items.csv
|
97
|
+
- spec/spec_helper.rb
|
98
|
+
homepage: https://github.com/Col/act_as_importable
|
99
|
+
licenses: []
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
none: false
|
112
|
+
requirements:
|
113
|
+
- - ! '>='
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubyforge_project:
|
118
|
+
rubygems_version: 1.8.24
|
119
|
+
signing_key:
|
120
|
+
specification_version: 3
|
121
|
+
summary: Helps import models from CSV files.
|
122
|
+
test_files:
|
123
|
+
- spec/act_as_importable/act_as_importable_spec.rb
|
124
|
+
- spec/fixtures/items.csv
|
125
|
+
- spec/spec_helper.rb
|