act_as_importable 0.0.1 → 0.0.2

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/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use --create 1.9.3@act_as_importable
@@ -8,8 +8,8 @@ Gem::Specification.new do |gem|
8
8
  gem.version = ActAsImportable::VERSION
9
9
  gem.authors = ["Colin Harris"]
10
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.}
11
+ gem.description = %q{Helps import records from CSV files.}
12
+ gem.summary = %q{Helps import records from CSV files.}
13
13
  gem.homepage = "https://github.com/Col/act_as_importable"
14
14
 
15
15
  gem.files = `git ls-files`.split($/)
Binary file
@@ -5,29 +5,31 @@ module ActAsImportable::Base
5
5
 
6
6
  module ClassMethods
7
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)
8
+ def import_csv_file(file, options = {})
9
+ import_csv_text(File.read(file), options)
14
10
  end
15
11
 
16
- ##
17
- # Imports csv text into a model
18
- def import_text(text, options = {})
12
+ def import_csv_text(text, options = {})
19
13
  csv = ::CSV.parse(text, :headers => true)
20
- csv.each do |row|
14
+ results = csv.map do |row|
21
15
  row = row.to_hash.with_indifferent_access
22
16
  import_record(row, options)
23
17
  end
24
18
  end
25
19
 
20
+ # Creates or updates a model record
21
+ # Existing records are found by the column(s) specified by the :uid option (default 'id').
22
+ # If the values for the uid columns are not provided the row will be ignored.
23
+ # If uid is set to nil it will import the row data as a new record.
26
24
  def import_record(row, options = {})
25
+ options = options.reverse_merge!(@default_import_options)
27
26
  row = filter_columns(row, options)
28
- record = find_existing_record(row, options)
29
- remove_unique_identifiers(row, options) if record
30
- record ||= self.new()
27
+ record = find_or_create_by(uid_values(row, options))
28
+ remove_uid_values_from_row(row, options)
29
+ update_record(record, row)
30
+ end
31
+
32
+ def update_record(record, row)
31
33
  update_associations(record, row)
32
34
  record.update_attributes(row)
33
35
  record.save!
@@ -39,55 +41,46 @@ module ActAsImportable::Base
39
41
  row
40
42
  end
41
43
 
42
- def remove_unique_identifiers(row, options = {})
43
- Array(options[:unique_identifier]).each do |field|
44
+ def uid_values(row, options)
45
+ Hash[Array(options[:uid]).map { |k| [k, row[k.to_sym]] }]
46
+ end
47
+
48
+ def remove_uid_values_from_row(row, options = {})
49
+ Array(options[:uid]).each do |field|
44
50
  row.delete(field)
45
51
  end
46
52
  end
47
53
 
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)
54
+ def update_associations(record, row)
52
55
  row.each_key do |key|
53
56
  key = key.to_s
54
57
  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
58
+ update_association(record, key, row[key])
60
59
  row.delete(key)
61
60
  end
62
61
  end
63
62
  end
64
63
 
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
64
+ def update_association(record, key, value)
65
+ association_name = key.split('.').first
66
+ uid_field = key.split('.').last
67
+ value = find_association_value_with_attribute(association_name, uid_field => value)
68
+ record.send("#{association_name}=", value) if value
74
69
  end
75
70
 
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
71
+ def find_association_value_with_attribute(name, attribute)
72
+ association = self.reflect_on_association(name.to_sym)
73
+ association.klass.where(attribute).first
74
+ end
75
+
76
+ # Note: This will be available by default in rails 4
77
+ def find_or_create_by(attributes, &block)
78
+ find_by(attributes) || create(attributes, &block)
79
+ end
80
+
81
+ # Note: This will by provided by default in rails 4
82
+ def find_by(attributes)
83
+ where(attributes).first
91
84
  end
92
85
 
93
86
  end
@@ -4,8 +4,16 @@ module ActAsImportable::Config
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  module ClassMethods
7
- def act_as_importable
7
+ def act_as_importable(options = {})
8
8
  include ActAsImportable::Base
9
+
10
+ @default_import_options = options
11
+ @default_import_options[:uid] ||= :id
12
+
13
+ # create a reader on the class to access the field name
14
+ class << self;
15
+ attr_reader :default_import_options
16
+ end
9
17
  end
10
18
  end
11
19
  end
@@ -0,0 +1,12 @@
1
+ require 'act_as_importable'
2
+ require 'rails'
3
+
4
+ module ActAsImportable
5
+ class Railtie < ::Rails::Railtie
6
+ initializer "act_as_importable.active_record" do |app|
7
+ ActiveSupport.on_load :active_record do
8
+ ActiveRecord::Base.send :include, ActAsImportable::Config
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,3 +1,3 @@
1
1
  module ActAsImportable
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -18,7 +18,7 @@ ActiveRecord::Base.connection.create_table(:items) do |t|
18
18
  end
19
19
 
20
20
  class Item < ActiveRecord::Base
21
- act_as_importable
21
+ act_as_importable :uid => 'name'
22
22
 
23
23
  belongs_to :category
24
24
  end
@@ -37,15 +37,22 @@ describe "an act_as_importable model" do
37
37
 
38
38
  subject { Item }
39
39
 
40
- it { should respond_to :import_file }
41
- it { should respond_to :import_text }
40
+ it { should respond_to :import_csv_file }
41
+ it { should respond_to :import_csv_text }
42
42
  it { should respond_to :import_record }
43
+ it { should respond_to :default_import_options }
44
+
45
+ let(:default_options) { {:uid => 'name'} }
46
+
47
+ it "should have the correct default import options" do
48
+ Item.default_import_options.should == default_options
49
+ end
43
50
 
44
51
  describe "import csv file" do
45
52
  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)
53
+ it 'should call import_text with contents of file' do
54
+ Item.should_receive(:import_csv_text).with(File.read(file), {})
55
+ Item.import_csv_file(file)
49
56
  end
50
57
  end
51
58
 
@@ -54,7 +61,7 @@ describe "an act_as_importable model" do
54
61
  it 'should call import_record with row hashes' do
55
62
  Item.should_receive(:import_record).with({'name' => 'Beer', 'price' => '2.5'}, {}).once
56
63
  Item.should_receive(:import_record).with({'name' => 'Apple', 'price' => '0.5'}, {}).once
57
- Item.import_text(text)
64
+ Item.import_csv_text(text)
58
65
  end
59
66
  end
60
67
 
@@ -70,11 +77,11 @@ describe "an act_as_importable model" do
70
77
  @existing_item = Item.create!(:name => 'Beer', :price => 1.0)
71
78
  end
72
79
  it "should update an existing record with matching unique identifier" do
73
- Item.import_record(row, :unique_identifier => 'name')
80
+ Item.import_record(row, :uid => 'name')
74
81
  @existing_item.reload.price.should == 2.5
75
82
  end
76
83
  it "should not create a new item" do
77
- expect { Item.import_record(row, :unique_identifier => 'name') }.to change { Item.count }.by(0)
84
+ expect { Item.import_record(row, :uid => 'name') }.to change { Item.count }.by(0)
78
85
  end
79
86
  end
80
87
  end
@@ -94,7 +101,7 @@ describe "an act_as_importable model" do
94
101
  let(:row) { {:name => 'Beer', :price => 2.5} }
95
102
 
96
103
  it 'should filter columns when importing each record' do
97
- Item.should_receive(:filter_columns).with(row, {}).and_return(row)
104
+ Item.should_receive(:filter_columns).with(row, default_options).and_return(row)
98
105
  Item.import_record(row)
99
106
  end
100
107
 
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.1
4
+ version: 0.0.2
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-03-05 00:00:00.000000000 Z
12
+ date: 2013-03-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -75,7 +75,7 @@ dependencies:
75
75
  - - ! '>='
76
76
  - !ruby/object:Gem::Version
77
77
  version: '0'
78
- description: Helps import models from CSV files.
78
+ description: Helps import records from CSV files.
79
79
  email:
80
80
  - col.w.harris@gmail.com
81
81
  executables: []
@@ -83,14 +83,17 @@ extensions: []
83
83
  extra_rdoc_files: []
84
84
  files:
85
85
  - .gitignore
86
+ - .rvmrc
86
87
  - Gemfile
87
88
  - LICENSE.txt
88
89
  - README.md
89
90
  - Rakefile
90
91
  - act_as_importable.gemspec
92
+ - db/act_as_importable.db
91
93
  - lib/act_as_importable.rb
92
94
  - lib/act_as_importable/base.rb
93
95
  - lib/act_as_importable/config.rb
96
+ - lib/act_as_importable/railtie.rb
94
97
  - lib/act_as_importable/version.rb
95
98
  - spec/act_as_importable/act_as_importable_spec.rb
96
99
  - spec/fixtures/items.csv
@@ -118,7 +121,7 @@ rubyforge_project:
118
121
  rubygems_version: 1.8.24
119
122
  signing_key:
120
123
  specification_version: 3
121
- summary: Helps import models from CSV files.
124
+ summary: Helps import records from CSV files.
122
125
  test_files:
123
126
  - spec/act_as_importable/act_as_importable_spec.rb
124
127
  - spec/fixtures/items.csv