active_record_importer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +172 -0
- data/Rakefile +6 -0
- data/active_record_importer.gemspec +30 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/db/migrate/1_active_record_importer_migration.rb +13 -0
- data/lib/active_record_importer.rb +27 -0
- data/lib/active_record_importer/attributes_builder.rb +67 -0
- data/lib/active_record_importer/batch_importer.rb +40 -0
- data/lib/active_record_importer/data_processor.rb +101 -0
- data/lib/active_record_importer/dispatcher.rb +48 -0
- data/lib/active_record_importer/engine.rb +4 -0
- data/lib/active_record_importer/errors.rb +17 -0
- data/lib/active_record_importer/find_options_builder.rb +52 -0
- data/lib/active_record_importer/helpers.rb +26 -0
- data/lib/active_record_importer/import_callbacker.rb +30 -0
- data/lib/active_record_importer/importable.rb +76 -0
- data/lib/active_record_importer/instance_builder.rb +56 -0
- data/lib/active_record_importer/options_builder.rb +45 -0
- data/lib/active_record_importer/railtie.rb +13 -0
- data/lib/active_record_importer/transition_processor.rb +39 -0
- data/lib/active_record_importer/version.rb +3 -0
- data/lib/app/models/import.rb +58 -0
- data/spec/active_record_importer_spec.rb +11 -0
- data/spec/spec_helper.rb +2 -0
- metadata +197 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 43724c0cedd35917d14e3439f96482c8f2357943
|
4
|
+
data.tar.gz: 7db97349cd0eafa87a2296906f56e6543c58395f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 22f7c0e2eb463b49e0a717d1ee4da685c111424681f20cbc2f578ebe5bbbab57277384f05715aca3abf22ffb25fe957efe8820ee916d6add08cc8ba82fa82e84
|
7
|
+
data.tar.gz: e90f8ab8ce49228e47441a3df8adcd214547fec041583912760e0792b1b6eff631f8378723d9cc73af23fbcc4605b8c58ee59578ca433a1c8a6070f413026502
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
4
|
+
|
5
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
|
6
|
+
|
7
|
+
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
8
|
+
|
9
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
10
|
+
|
11
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
12
|
+
|
13
|
+
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 TODO: Write your name
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
# ActiveRecordImporter
|
2
|
+
|
3
|
+
Supports only Rails 4... (I'll release soon an update to support Rails 5)
|
4
|
+
|
5
|
+
This gem helps you insert/update records easily. For now, it only accepts CSV file.
|
6
|
+
This also helps you monitor how many rows are imported, and how many rows failed.
|
7
|
+
I'll release an update to enable this on background job.
|
8
|
+
This gem allows you to easily import to any model with few configurations.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'active_record_importer'
|
16
|
+
```
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install active_record_importer
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
Simple usage for now:
|
29
|
+
|
30
|
+
### Create Import table/model
|
31
|
+
I'll add a generator on my next release
|
32
|
+
#### DB Migration:
|
33
|
+
```ruby
|
34
|
+
class ActiveRecordImporterMigration < ActiveRecord::Migration
|
35
|
+
def change
|
36
|
+
create_table :imports do |t|
|
37
|
+
t.attachment :file
|
38
|
+
t.attachment :failed_file
|
39
|
+
t.text :properties
|
40
|
+
t.string :resource, null: false
|
41
|
+
t.integer :imported_rows, default: 0
|
42
|
+
t.integer :failed_rows, default: 0
|
43
|
+
t.timestamps
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
```
|
48
|
+
|
49
|
+
#### Import Model:
|
50
|
+
```ruby
|
51
|
+
class Import < ActiveRecord::Base
|
52
|
+
extend Enumerize
|
53
|
+
store :properties, accessors: %i(insert_method find_options batch_size)
|
54
|
+
|
55
|
+
enumerize :insert_method,
|
56
|
+
in: %w(insert upsert error_duplicate),
|
57
|
+
default: :upsert
|
58
|
+
|
59
|
+
has_attached_file :file
|
60
|
+
|
61
|
+
attr_accessor :execute_on_create
|
62
|
+
|
63
|
+
validates :resource, presence: true
|
64
|
+
validate :check_presence_of_find_options
|
65
|
+
validates_attachment :file,
|
66
|
+
content_type: {
|
67
|
+
content_type: %w(text/plain text/csv)
|
68
|
+
}
|
69
|
+
|
70
|
+
after_create :execute, if: :execute_on_create
|
71
|
+
|
72
|
+
# I'll add import options in the next release
|
73
|
+
# accepts_nested_attributes_for :import_options, allow_destroy: true
|
74
|
+
|
75
|
+
def execute
|
76
|
+
resource_class.import!(self, execute_on_create)
|
77
|
+
end
|
78
|
+
|
79
|
+
def resource_class
|
80
|
+
resource.safe_constantize
|
81
|
+
end
|
82
|
+
|
83
|
+
def batch_size
|
84
|
+
super.to_i
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Override this if you prefer have
|
89
|
+
# a private permissions or you have
|
90
|
+
# private methods for reading files
|
91
|
+
##
|
92
|
+
def import_file
|
93
|
+
local_path? ? file.path : file.url
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def check_presence_of_find_options
|
99
|
+
return if insert_method.insert?
|
100
|
+
errors.add(:find_options, "can't be blank") if find_options.blank?
|
101
|
+
end
|
102
|
+
|
103
|
+
def local_path?
|
104
|
+
File.exist? import_file.file.path
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
Add `acts_as_importable` to a model to make it importable
|
110
|
+
```ruby
|
111
|
+
class User < ActiveRecord::Base
|
112
|
+
acts_as_importable
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
You may also add import options:
|
117
|
+
```ruby
|
118
|
+
class User < ActiveRecord::Base
|
119
|
+
acts_as_importable default_attributes: { first_name: 'Juan',
|
120
|
+
last_name: 'dela Cruz' },
|
121
|
+
find_options: %i(email),
|
122
|
+
before_save: Proc.new { |user| user.password = 'temporarypassword123' }
|
123
|
+
end
|
124
|
+
```
|
125
|
+
|
126
|
+
You may also add some options from the SmarterCSV gem:
|
127
|
+
|
128
|
+
| Option | Default
|
129
|
+
--------------------------------------------------------------
|
130
|
+
| :convert_values_to_metric | nil
|
131
|
+
| :value_converters | nil
|
132
|
+
| :remove_empty_values | false
|
133
|
+
| :comment_regexp | Regexp.new(/^#=>/)
|
134
|
+
| :force_utf8 | true
|
135
|
+
| :chunk_size | 500
|
136
|
+
| :col_sep | ","
|
137
|
+
|
138
|
+
#### I'll add more options SOON!
|
139
|
+
|
140
|
+
#### Imports Controller:
|
141
|
+
```ruby
|
142
|
+
class ImportsController < ApplicationController
|
143
|
+
|
144
|
+
def create
|
145
|
+
@import = Import.create!(import_params)
|
146
|
+
@import.execute_on_create = true
|
147
|
+
@import.execute
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
You may include execute_on_create as a checkbox field in your Import form so you don't have to
|
154
|
+
include `@import.execute_on_create = true` in your controller.
|
155
|
+
|
156
|
+
|
157
|
+
|
158
|
+
## Development
|
159
|
+
|
160
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
161
|
+
|
162
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
163
|
+
|
164
|
+
## Contributing
|
165
|
+
|
166
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/michaelnera/active_record_importer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
|
167
|
+
|
168
|
+
|
169
|
+
## License
|
170
|
+
|
171
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
172
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'active_record_importer/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'active_record_importer'
|
8
|
+
spec.version = ActiveRecordImporter::VERSION
|
9
|
+
spec.authors = ['Michael Nera']
|
10
|
+
spec.email = ['kapitan_03@yahoo.com']
|
11
|
+
|
12
|
+
spec.summary = 'Active Record Importer'
|
13
|
+
spec.description = 'Smart gem for importing rails models'
|
14
|
+
spec.homepage = 'https://github.com/michaelnera/active_record_importer'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files`.split($/)
|
18
|
+
spec.bindir = 'exe'
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_development_dependency 'rspec', '~> 2.99'
|
23
|
+
|
24
|
+
spec.add_runtime_dependency 'activerecord', '>= 4.0'
|
25
|
+
spec.add_runtime_dependency 'activesupport', '>= 4.0'
|
26
|
+
spec.add_runtime_dependency 'enumerize', '>= 2.0', '~> 2.0.1'
|
27
|
+
spec.add_runtime_dependency 'virtus', '>= 1.0', '~> 1.0'
|
28
|
+
spec.add_runtime_dependency 'smarter_csv', '>= 1.0', '~> 1.0'
|
29
|
+
spec.add_runtime_dependency 'paperclip', '>= 4.0', '~> 4.0'
|
30
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "active_record_importer"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
class ActiveRecordImporterMigration < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :imports do |t|
|
4
|
+
t.attachment :file
|
5
|
+
t.attachment :failed_file
|
6
|
+
t.text :properties
|
7
|
+
t.string :resource, null: false
|
8
|
+
t.integer :imported_rows, default: 0
|
9
|
+
t.integer :failed_rows, default: 0
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'active_record_importer/version'
|
2
|
+
require 'active_support/core_ext/module'
|
3
|
+
require 'virtus'
|
4
|
+
require 'enumerize'
|
5
|
+
require 'smarter_csv'
|
6
|
+
require 'paperclip'
|
7
|
+
|
8
|
+
module ActiveRecordImporter
|
9
|
+
extend ActiveSupport::Autoload
|
10
|
+
|
11
|
+
::IMPORTABLES = []
|
12
|
+
|
13
|
+
autoload :BatchImporter, 'active_record_importer/batch_importer'
|
14
|
+
autoload :DataProcessor, 'active_record_importer/data_processor'
|
15
|
+
autoload :Dispatcher, 'active_record_importer/dispatcher'
|
16
|
+
autoload :Errors, 'active_record_importer/errors'
|
17
|
+
autoload :Importable, 'active_record_importer/importable'
|
18
|
+
autoload :InstanceBuilder, 'active_record_importer/instance_builder'
|
19
|
+
autoload :OptionsBuilder, 'active_record_importer/options_builder'
|
20
|
+
autoload :AttributesBuilder, 'active_record_importer/attributes_builder'
|
21
|
+
autoload :FindOptionsBuilder, 'active_record_importer/find_options_builder'
|
22
|
+
autoload :TransitionProcessor, 'active_record_importer/transition_processor'
|
23
|
+
autoload :ImportCallbacker, 'active_record_importer/import_callbacker'
|
24
|
+
autoload :Helpers, 'active_record_importer/helpers'
|
25
|
+
|
26
|
+
require 'active_record_importer/railtie' if defined?(Rails) && Rails::VERSION::MAJOR >= 3
|
27
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
class AttributesBuilder
|
3
|
+
include Helpers
|
4
|
+
|
5
|
+
attr_reader :importable, :row_attrs, :processed_attrs
|
6
|
+
|
7
|
+
delegate :importer_options, to: :importable
|
8
|
+
delegate :importable_columns,
|
9
|
+
:default_attributes,
|
10
|
+
:find_assoc_opts,
|
11
|
+
to: :importer_options
|
12
|
+
|
13
|
+
def initialize(importable, row_attrs)
|
14
|
+
@importable = importable
|
15
|
+
@row_attrs = row_attrs
|
16
|
+
@processed_attrs = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def build
|
20
|
+
force_encode_attributes
|
21
|
+
fetch_time_attributes
|
22
|
+
processed_attrs
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def default_attrs
|
28
|
+
def_attrs = { importing: true }
|
29
|
+
default_attributes.each do |key, value|
|
30
|
+
def_attrs[key] = fetch_value(value)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def force_encode_attributes
|
35
|
+
@processed_attrs = force_utf8_encode(merged_attributes)
|
36
|
+
end
|
37
|
+
|
38
|
+
def fetch_time_attributes
|
39
|
+
@processed_attrs.merge!(time_attributes(processed_attrs))
|
40
|
+
end
|
41
|
+
|
42
|
+
def fetch_value(value)
|
43
|
+
case value
|
44
|
+
when Proc
|
45
|
+
value.call(row_attrs)
|
46
|
+
when Symbol
|
47
|
+
importable.send(value, row_attrs)
|
48
|
+
else
|
49
|
+
value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def merged_attributes
|
54
|
+
attributes = row_attrs.slice(*importable_columns)
|
55
|
+
row_attrs = attributes.inject({}) do |row_attrs, key_value|
|
56
|
+
row_attrs[key_value.first] = key_value.last if key_value.last.present?
|
57
|
+
row_attrs
|
58
|
+
end
|
59
|
+
default_attrs.merge(row_attrs)
|
60
|
+
end
|
61
|
+
|
62
|
+
def has_column?(column)
|
63
|
+
return if column.blank?
|
64
|
+
importable_columns.include?(column.to_sym)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
class BatchImporter
|
3
|
+
|
4
|
+
attr_reader :data, :import
|
5
|
+
|
6
|
+
def initialize(import, data)
|
7
|
+
@import = import
|
8
|
+
@data = data
|
9
|
+
end
|
10
|
+
|
11
|
+
def process!
|
12
|
+
@imported_count, @failed_count = 0, 0
|
13
|
+
|
14
|
+
data.each do |row|
|
15
|
+
next if row.blank?
|
16
|
+
processor = DataProcessor.new(import, row.symbolize_keys!)
|
17
|
+
processor.process ? @imported_count += 1 : @failed_count += 1
|
18
|
+
end
|
19
|
+
|
20
|
+
set_import_count
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def set_import_count
|
26
|
+
Import.update_counters(import.id, imported_rows: @imported_count)
|
27
|
+
Import.update_counters(import.id, failed_rows: @failed_count)
|
28
|
+
end
|
29
|
+
|
30
|
+
def importable
|
31
|
+
import.resource.safe_constantize
|
32
|
+
end
|
33
|
+
|
34
|
+
delegate :importer_options, to: :importable
|
35
|
+
|
36
|
+
def csv_options
|
37
|
+
importer_options.csv_opts.to_hash
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
class DataProcessor
|
3
|
+
attr_reader :importable, :import, :instance, :attributes,
|
4
|
+
:row_errors, :row_attrs, :find_attributes
|
5
|
+
|
6
|
+
delegate :importer_options,
|
7
|
+
to: :importable
|
8
|
+
|
9
|
+
def initialize(import, row_attrs)
|
10
|
+
@import = import
|
11
|
+
@importable = import.resource.safe_constantize
|
12
|
+
@row_attrs = row_attrs
|
13
|
+
end
|
14
|
+
|
15
|
+
def process
|
16
|
+
fetch_instance_attributes
|
17
|
+
fetch_find_attributes
|
18
|
+
create_instance
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def create_instance
|
24
|
+
ActiveRecord::Base.transaction do
|
25
|
+
begin
|
26
|
+
@instance =
|
27
|
+
InstanceBuilder.new(
|
28
|
+
import, find_attributes,
|
29
|
+
attributes_without_state_machine_attrs
|
30
|
+
).build
|
31
|
+
|
32
|
+
methods_after_upsert
|
33
|
+
true
|
34
|
+
rescue => exception
|
35
|
+
append_errors(exception, true)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def fetch_instance_attributes
|
41
|
+
@attributes = AttributesBuilder.new(
|
42
|
+
importable, row_attrs
|
43
|
+
).build
|
44
|
+
rescue => exception
|
45
|
+
append_errors(exception)
|
46
|
+
end
|
47
|
+
|
48
|
+
def fetch_find_attributes
|
49
|
+
@find_attributes = FindOptionsBuilder.new(
|
50
|
+
resource: import.resource,
|
51
|
+
find_options: import.find_options,
|
52
|
+
attrs: attributes
|
53
|
+
).build
|
54
|
+
rescue => exception
|
55
|
+
append_errors(exception)
|
56
|
+
end
|
57
|
+
|
58
|
+
def methods_after_upsert
|
59
|
+
return if instance.blank? || !instance.persisted?
|
60
|
+
|
61
|
+
state_transitions
|
62
|
+
run_after_save_callbacks
|
63
|
+
end
|
64
|
+
|
65
|
+
delegate :after_save,
|
66
|
+
:state_machine_attr,
|
67
|
+
to: :importer_options
|
68
|
+
|
69
|
+
def state_transitions
|
70
|
+
return if state_machine_attr.blank?
|
71
|
+
|
72
|
+
state_machine_attr.each do |attr|
|
73
|
+
state = row_attrs[attr.to_sym]
|
74
|
+
next if state.blank? || state == instance.send(attr)
|
75
|
+
TransitionProcessor.new(instance, state, attr).transit
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def attributes_without_state_machine_attrs
|
80
|
+
attributes.except(*state_machine_attr)
|
81
|
+
end
|
82
|
+
|
83
|
+
def skip_callbacks?
|
84
|
+
after_save.blank? || instance.blank? || instance.new_record?
|
85
|
+
end
|
86
|
+
|
87
|
+
def run_after_save_callbacks
|
88
|
+
return if skip_callbacks?
|
89
|
+
|
90
|
+
ImportCallbacker.new(instance, after_save).call
|
91
|
+
end
|
92
|
+
|
93
|
+
def append_errors(error, rollback = false)
|
94
|
+
return if error.blank?
|
95
|
+
|
96
|
+
message = error.is_a?(Exception) ? error.message : error
|
97
|
+
@row_errors = message
|
98
|
+
fail ActiveRecord::Rollback if rollback
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
class Dispatcher
|
3
|
+
attr_reader :import, :execute
|
4
|
+
|
5
|
+
def initialize(import_id, execute = true)
|
6
|
+
@import = Import.find(import_id)
|
7
|
+
@execute = execute
|
8
|
+
end
|
9
|
+
|
10
|
+
def call
|
11
|
+
divide_and_conquer
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def divide_and_conquer
|
17
|
+
File.open(import.import_file, 'r:bom|utf-8') do |file|
|
18
|
+
SmarterCSV.process(file, csv_options) do |collection|
|
19
|
+
queue_or_execute(collection)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def csv_options
|
25
|
+
klass = import.resource.safe_constantize
|
26
|
+
opts = klass_csv_opts(klass)
|
27
|
+
return opts if import.batch_size.blank? || import.batch_size < 1
|
28
|
+
opts.merge(chunk_size: import.batch_size)
|
29
|
+
end
|
30
|
+
|
31
|
+
def klass_csv_opts(klass)
|
32
|
+
klass.importer_options.csv_opts.to_hash
|
33
|
+
end
|
34
|
+
|
35
|
+
def queue_or_execute(collection)
|
36
|
+
process_import(collection) if execute
|
37
|
+
queue(collection)
|
38
|
+
end
|
39
|
+
|
40
|
+
def process_import(collection)
|
41
|
+
BatchImporter.new(import, collection).process!
|
42
|
+
end
|
43
|
+
|
44
|
+
def queue(collection)
|
45
|
+
# To follow
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
module Errors
|
3
|
+
class InvalidTransition < StandardError
|
4
|
+
def initialize
|
5
|
+
super 'Transition is invalid'
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class MissingFindByOption < StandardError
|
10
|
+
def initialize
|
11
|
+
super 'Find by option for your csv is missing or incorrect.'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class DuplicateRecord < StandardError; end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
class FindOptionsBuilder
|
3
|
+
include Virtus.model
|
4
|
+
include Helpers
|
5
|
+
|
6
|
+
attribute :resource, String
|
7
|
+
attribute :attrs, Hash, default: {}
|
8
|
+
attribute :find_options, String
|
9
|
+
attribute :prefix, String
|
10
|
+
|
11
|
+
def build
|
12
|
+
get_find_opts
|
13
|
+
slice_attributes
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def klass
|
19
|
+
resource.safe_constantize
|
20
|
+
end
|
21
|
+
|
22
|
+
delegate :importer_options, to: :klass
|
23
|
+
|
24
|
+
delegate :required_attributes, to: :importer_options
|
25
|
+
|
26
|
+
def get_find_opts
|
27
|
+
@options = strip_and_symbolize
|
28
|
+
@options ||= importer_options.find_options || required_attributes
|
29
|
+
@options
|
30
|
+
end
|
31
|
+
|
32
|
+
def slice_attributes
|
33
|
+
return attrs.slice(*@options).compact if prefix.blank?
|
34
|
+
|
35
|
+
@options.inject({}) do |attr, key|
|
36
|
+
attr[key] = attrs[prefixed_key(key)].presence
|
37
|
+
attr
|
38
|
+
end.compact
|
39
|
+
end
|
40
|
+
|
41
|
+
def prefixed_key(key)
|
42
|
+
"#{prefix}#{key}".to_sym
|
43
|
+
end
|
44
|
+
|
45
|
+
def strip_and_symbolize
|
46
|
+
return if find_options.blank?
|
47
|
+
find_options.split(',').map do |key|
|
48
|
+
key.strip.to_sym
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
def parse_datetime(datetime = nil)
|
5
|
+
return if datetime.blank?
|
6
|
+
Time.parse(datetime)
|
7
|
+
end
|
8
|
+
|
9
|
+
def force_utf8_encode(data = {})
|
10
|
+
return data if data.blank?
|
11
|
+
|
12
|
+
data.keys.each do |key|
|
13
|
+
data[key] = data[key].force_encoding('UTF-8') if data[key].is_a?(String)
|
14
|
+
end
|
15
|
+
|
16
|
+
data
|
17
|
+
end
|
18
|
+
|
19
|
+
def time_attributes(data = {})
|
20
|
+
attrs = {}
|
21
|
+
attrs[:created_at] = parse_datetime(data[:created_at]) || Time.now
|
22
|
+
attrs[:updated_at] = parse_datetime(data[:updated_at]) || attrs[:created_at]
|
23
|
+
attrs
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
class ImportCallbacker
|
3
|
+
attr_reader :object, :callback_methods
|
4
|
+
|
5
|
+
def initialize(object, callback_methods)
|
6
|
+
@object = object
|
7
|
+
@callback_methods = callback_methods
|
8
|
+
end
|
9
|
+
|
10
|
+
def call
|
11
|
+
case callback_methods
|
12
|
+
when Array
|
13
|
+
run_each_callbacks
|
14
|
+
when Symbol
|
15
|
+
object.send(callback)
|
16
|
+
when Proc
|
17
|
+
callback.call(object)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def run_each_callbacks
|
24
|
+
callback_methods.each do |callback|
|
25
|
+
object.send(callback) if callback.is_a?(Symbol)
|
26
|
+
callback.call(object) if callback.is_a?(Proc)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
module Importable
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
##
|
9
|
+
# Make a model importable
|
10
|
+
# This will allow a model to use the importer
|
11
|
+
#
|
12
|
+
# class User < ActiveRecord::Base
|
13
|
+
# acts_as_importable
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# You may also add options:
|
17
|
+
# class User < ActiveRecord::Base
|
18
|
+
# acts_as_importable default_attributes: {first_name: 'Michael', surname: 'Nera'}
|
19
|
+
# end
|
20
|
+
##
|
21
|
+
def acts_as_importable(options = {})
|
22
|
+
@@importer_options = OptionsBuilder.build(options.merge(allowed_columns_hash(options)))
|
23
|
+
::IMPORTABLES << self.name unless ::IMPORTABLES.include?(self.name)
|
24
|
+
|
25
|
+
include ActiveRecordImporter::Importable::InstanceMethods
|
26
|
+
end
|
27
|
+
|
28
|
+
def importer_options
|
29
|
+
@@importer_options
|
30
|
+
end
|
31
|
+
|
32
|
+
def import!(import_object, execute = true)
|
33
|
+
ActiveRecordImporter::Dispatcher.new(
|
34
|
+
import_object.id, execute).call
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def allowed_columns_hash(options = {})
|
40
|
+
{
|
41
|
+
importable_columns: allowed_columns(options)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def allowed_columns(options = {})
|
46
|
+
all_columns + store_accessors(options) - excluded_columns(options)
|
47
|
+
end
|
48
|
+
|
49
|
+
def all_columns
|
50
|
+
return [] unless (self.respond_to?(:table_exists?) && self.table_exists?)
|
51
|
+
self.columns.map { |column| column.name.to_sym }
|
52
|
+
end
|
53
|
+
|
54
|
+
def store_accessors(options = {})
|
55
|
+
options.fetch(:store_accessors, [])
|
56
|
+
end
|
57
|
+
|
58
|
+
def importable_columns
|
59
|
+
importer_options.importable_columns
|
60
|
+
end
|
61
|
+
|
62
|
+
def excluded_columns(options = {})
|
63
|
+
columns = [:id]
|
64
|
+
columns + options.fetch(:exclude_columns, [])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
module InstanceMethods
|
69
|
+
attr_accessor :importing
|
70
|
+
|
71
|
+
def importing?
|
72
|
+
@importing ||= false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
class InstanceBuilder
|
3
|
+
attr_reader :attributes, :find_attributes, :import
|
4
|
+
|
5
|
+
def initialize(import, find_attributes, attributes)
|
6
|
+
@import = import
|
7
|
+
@find_attributes = find_attributes
|
8
|
+
@attributes = attributes
|
9
|
+
end
|
10
|
+
|
11
|
+
def build
|
12
|
+
instance = initialize_instance
|
13
|
+
process_data(instance)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
delegate :insert_method, to: :import
|
19
|
+
|
20
|
+
def initialize_instance
|
21
|
+
return klass.new if insert_method.insert?
|
22
|
+
|
23
|
+
fail Errors::MissingFindByOption if find_attributes.blank?
|
24
|
+
klass.find_or_initialize_by(find_attributes)
|
25
|
+
end
|
26
|
+
|
27
|
+
def klass
|
28
|
+
import.resource.safe_constantize
|
29
|
+
end
|
30
|
+
|
31
|
+
delegate :importer_options, to: :klass
|
32
|
+
delegate :before_save, to: :importer_options
|
33
|
+
|
34
|
+
def process_data(instance)
|
35
|
+
fail Errors::DuplicateRecord if error_duplicate?(instance)
|
36
|
+
|
37
|
+
before_save_callback(instance)
|
38
|
+
assign_attrs_and_save!(instance)
|
39
|
+
end
|
40
|
+
|
41
|
+
def before_save_callback(instance)
|
42
|
+
return if before_save.blank?
|
43
|
+
ImportCallbacker.new(instance, before_save).call
|
44
|
+
end
|
45
|
+
|
46
|
+
def assign_attrs_and_save!(instance)
|
47
|
+
instance.attributes = attributes
|
48
|
+
instance.save!
|
49
|
+
instance
|
50
|
+
end
|
51
|
+
|
52
|
+
def error_duplicate?(instance)
|
53
|
+
instance.persisted? && insert_method.error_duplicate?
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
class OptionsBuilder
|
3
|
+
def self.build(options = {})
|
4
|
+
ImporterOptions.new(options)
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
#
|
9
|
+
# SmarterCSV is used to process the csv file
|
10
|
+
# https://github.com/tilo/smarter_csv
|
11
|
+
# I used some of it's options
|
12
|
+
# Please refer to it's documentation
|
13
|
+
#
|
14
|
+
class CsvOptions
|
15
|
+
include Virtus.value_object
|
16
|
+
|
17
|
+
values do
|
18
|
+
attribute :convert_values_to_numeric
|
19
|
+
attribute :value_converters, Hash, default: nil
|
20
|
+
attribute :remove_empty_values, Boolean, default: false
|
21
|
+
attribute :comment_regexp, Regexp, default: Regexp.new(/^#=>/)
|
22
|
+
attribute :force_utf8, Boolean, default: true
|
23
|
+
attribute :chunk_size, Integer, default: 500
|
24
|
+
attribute :col_sep, String, default: ','
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Import Option for every IMPORTABLE model
|
30
|
+
#
|
31
|
+
class ImporterOptions
|
32
|
+
include Virtus.model
|
33
|
+
|
34
|
+
attribute :find_options, Array
|
35
|
+
attribute :exclude_from_find_options, Array
|
36
|
+
attribute :scope, Symbol
|
37
|
+
attribute :importable_columns, Array
|
38
|
+
attribute :default_attributes, Hash
|
39
|
+
attribute :csv_opts, CsvOptions, default: CsvOptions.new
|
40
|
+
attribute :find_assoc_opts, Hash
|
41
|
+
attribute :before_save
|
42
|
+
attribute :after_save
|
43
|
+
attribute :state_machine_attr, Array
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rails'
|
2
|
+
|
3
|
+
module ActiveRecordImporter
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
|
6
|
+
initializer "active_record_importer.active_record" do |_app|
|
7
|
+
ActiveSupport.on_load :active_record do
|
8
|
+
include ActiveRecordImporter::Importable
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
class TransitionProcessor
|
3
|
+
|
4
|
+
attr_reader :object, :column, :new_state
|
5
|
+
|
6
|
+
def initialize(object, new_state, column = :state)
|
7
|
+
@object = object
|
8
|
+
@new_state = new_state
|
9
|
+
@column = column
|
10
|
+
end
|
11
|
+
|
12
|
+
def transit
|
13
|
+
fire_event!(transit_event)
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def fire_event!(event)
|
20
|
+
fail Errors::InvalidTransition if event.blank?
|
21
|
+
object.send(event)
|
22
|
+
end
|
23
|
+
|
24
|
+
def transit_event
|
25
|
+
state_transitions.each do |trans|
|
26
|
+
return trans.event if trans.from == state_was && trans.to == new_state
|
27
|
+
end
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def state_transitions
|
32
|
+
object.send("#{column}_transitions")
|
33
|
+
end
|
34
|
+
|
35
|
+
def state_was
|
36
|
+
object.send("#{column}_was")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
class Import < ActiveRecord::Base
|
3
|
+
extend Enumerize
|
4
|
+
store :properties, accessors: %i(insert_method find_options batch_size)
|
5
|
+
|
6
|
+
enumerize :insert_method,
|
7
|
+
in: %w(insert upsert error_duplicate),
|
8
|
+
default: :upsert
|
9
|
+
|
10
|
+
has_attached_file :file
|
11
|
+
|
12
|
+
attr_accessor :execute_on_create
|
13
|
+
|
14
|
+
validates :resource, presence: true
|
15
|
+
validate :check_presence_of_find_options
|
16
|
+
validates_attachment :file,
|
17
|
+
content_type: {
|
18
|
+
content_type: %w(text/plain text/csv)
|
19
|
+
}
|
20
|
+
|
21
|
+
after_create :execute, if: :execute_on_create
|
22
|
+
|
23
|
+
# I'll add import options in the next release
|
24
|
+
# accepts_nested_attributes_for :import_options, allow_destroy: true
|
25
|
+
|
26
|
+
def execute
|
27
|
+
resource_class.import!(self, execute_on_create)
|
28
|
+
end
|
29
|
+
|
30
|
+
def resource_class
|
31
|
+
resource.safe_constantize
|
32
|
+
end
|
33
|
+
|
34
|
+
def batch_size
|
35
|
+
super.to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Override this if you prefer have
|
40
|
+
# a private permissions or you have
|
41
|
+
# private methods for reading files
|
42
|
+
##
|
43
|
+
def import_file
|
44
|
+
local_path? ? file.path : file.url
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def check_presence_of_find_options
|
50
|
+
return if insert_method.insert?
|
51
|
+
errors.add(:find_options, "can't be blank") if find_options.blank?
|
52
|
+
end
|
53
|
+
|
54
|
+
def local_path?
|
55
|
+
File.exist? import_file.file.path
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_record_importer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Nera
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-02-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.99'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.99'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '4.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '4.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activesupport
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: enumerize
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.0'
|
62
|
+
- - "~>"
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: 2.0.1
|
65
|
+
type: :runtime
|
66
|
+
prerelease: false
|
67
|
+
version_requirements: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '2.0'
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 2.0.1
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: virtus
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '1.0'
|
82
|
+
- - "~>"
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '1.0'
|
85
|
+
type: :runtime
|
86
|
+
prerelease: false
|
87
|
+
version_requirements: !ruby/object:Gem::Requirement
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '1.0'
|
92
|
+
- - "~>"
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '1.0'
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: smarter_csv
|
97
|
+
requirement: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '1.0'
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '1.0'
|
105
|
+
type: :runtime
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '1.0'
|
112
|
+
- - "~>"
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '1.0'
|
115
|
+
- !ruby/object:Gem::Dependency
|
116
|
+
name: paperclip
|
117
|
+
requirement: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '4.0'
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '4.0'
|
125
|
+
type: :runtime
|
126
|
+
prerelease: false
|
127
|
+
version_requirements: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '4.0'
|
132
|
+
- - "~>"
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '4.0'
|
135
|
+
description: Smart gem for importing rails models
|
136
|
+
email:
|
137
|
+
- kapitan_03@yahoo.com
|
138
|
+
executables: []
|
139
|
+
extensions: []
|
140
|
+
extra_rdoc_files: []
|
141
|
+
files:
|
142
|
+
- ".gitignore"
|
143
|
+
- ".rspec"
|
144
|
+
- ".travis.yml"
|
145
|
+
- CODE_OF_CONDUCT.md
|
146
|
+
- Gemfile
|
147
|
+
- LICENSE.txt
|
148
|
+
- README.md
|
149
|
+
- Rakefile
|
150
|
+
- active_record_importer.gemspec
|
151
|
+
- bin/console
|
152
|
+
- bin/setup
|
153
|
+
- db/migrate/1_active_record_importer_migration.rb
|
154
|
+
- lib/active_record_importer.rb
|
155
|
+
- lib/active_record_importer/attributes_builder.rb
|
156
|
+
- lib/active_record_importer/batch_importer.rb
|
157
|
+
- lib/active_record_importer/data_processor.rb
|
158
|
+
- lib/active_record_importer/dispatcher.rb
|
159
|
+
- lib/active_record_importer/engine.rb
|
160
|
+
- lib/active_record_importer/errors.rb
|
161
|
+
- lib/active_record_importer/find_options_builder.rb
|
162
|
+
- lib/active_record_importer/helpers.rb
|
163
|
+
- lib/active_record_importer/import_callbacker.rb
|
164
|
+
- lib/active_record_importer/importable.rb
|
165
|
+
- lib/active_record_importer/instance_builder.rb
|
166
|
+
- lib/active_record_importer/options_builder.rb
|
167
|
+
- lib/active_record_importer/railtie.rb
|
168
|
+
- lib/active_record_importer/transition_processor.rb
|
169
|
+
- lib/active_record_importer/version.rb
|
170
|
+
- lib/app/models/import.rb
|
171
|
+
- spec/active_record_importer_spec.rb
|
172
|
+
- spec/spec_helper.rb
|
173
|
+
homepage: https://github.com/michaelnera/active_record_importer
|
174
|
+
licenses:
|
175
|
+
- MIT
|
176
|
+
metadata: {}
|
177
|
+
post_install_message:
|
178
|
+
rdoc_options: []
|
179
|
+
require_paths:
|
180
|
+
- lib
|
181
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
182
|
+
requirements:
|
183
|
+
- - ">="
|
184
|
+
- !ruby/object:Gem::Version
|
185
|
+
version: '0'
|
186
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
187
|
+
requirements:
|
188
|
+
- - ">="
|
189
|
+
- !ruby/object:Gem::Version
|
190
|
+
version: '0'
|
191
|
+
requirements: []
|
192
|
+
rubyforge_project:
|
193
|
+
rubygems_version: 2.4.6
|
194
|
+
signing_key:
|
195
|
+
specification_version: 4
|
196
|
+
summary: Active Record Importer
|
197
|
+
test_files: []
|