active_record_importer 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +89 -18
- data/lib/active_record_importer.rb +7 -3
- data/lib/active_record_importer/attribute/attributes_builder.rb +69 -0
- data/lib/active_record_importer/attribute/find_options_builder.rb +54 -0
- data/lib/active_record_importer/attribute/helpers.rb +28 -0
- data/lib/active_record_importer/batch_importer.rb +16 -4
- data/lib/active_record_importer/data_processor.rb +2 -2
- data/lib/active_record_importer/errors.rb +5 -1
- data/lib/active_record_importer/failed_file_builder.rb +81 -0
- data/lib/active_record_importer/version.rb +1 -1
- data/lib/app/models/import.rb +23 -6
- metadata +6 -5
- data/lib/active_record_importer/attributes_builder.rb +0 -67
- data/lib/active_record_importer/find_options_builder.rb +0 -52
- data/lib/active_record_importer/helpers.rb +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 07cd0e7debf64daad8259915aa82f993c1c86f1c
|
4
|
+
data.tar.gz: 98df0543814ef29f1bbe2ec031c9f02394db723e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 521db635648cfe9da9d5ce938f58c519db6e6e16c7428df3efa25a251fce72f655c17ee98d61f9d98ac949b377107f07be5377b531e0ac1597edf4e9238aa5b2
|
7
|
+
data.tar.gz: 407647a49e620e64cc47c125f106017b132e24c44fb039d4e882f6ceea7f5b3ec9ed90ec65d8ed0e638ef79590c3bf660f50bf8f94a3557f765ed260facd09c6
|
data/README.md
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# ActiveRecordImporter
|
2
2
|
|
3
|
-
Supports only Rails 4
|
3
|
+
Supports only Rails 4 and 5
|
4
4
|
|
5
5
|
This gem helps you insert/update records easily. For now, it only accepts CSV file.
|
6
6
|
This also helps you monitor how many rows are imported, and how many rows failed.
|
7
|
+
This gem also allows you to easily import to any model with few configurations.
|
8
|
+
|
7
9
|
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
|
|
10
11
|
## Installation
|
11
12
|
|
@@ -46,7 +47,7 @@ class ActiveRecordImporterMigration < ActiveRecord::Migration
|
|
46
47
|
end
|
47
48
|
```
|
48
49
|
|
49
|
-
#### Import Model:
|
50
|
+
#### Add Import Model:
|
50
51
|
```ruby
|
51
52
|
class Import < ActiveRecord::Base
|
52
53
|
extend Enumerize
|
@@ -57,6 +58,7 @@ class Import < ActiveRecord::Base
|
|
57
58
|
default: :upsert
|
58
59
|
|
59
60
|
has_attached_file :file
|
61
|
+
has_attached_file :failed_file
|
60
62
|
|
61
63
|
attr_accessor :execute_on_create
|
62
64
|
|
@@ -67,15 +69,22 @@ class Import < ActiveRecord::Base
|
|
67
69
|
content_type: %w(text/plain text/csv)
|
68
70
|
}
|
69
71
|
|
70
|
-
|
72
|
+
validates_attachment :failed_file,
|
73
|
+
content_type: {
|
74
|
+
content_type: %w(text/plain text/csv)
|
75
|
+
}
|
71
76
|
|
72
|
-
# I'll add import options in the next release
|
77
|
+
# I'll add import options in the next major release
|
73
78
|
# accepts_nested_attributes_for :import_options, allow_destroy: true
|
74
79
|
|
75
80
|
def execute
|
76
81
|
resource_class.import!(self, execute_on_create)
|
77
82
|
end
|
78
83
|
|
84
|
+
def execute!
|
85
|
+
resource_class.import!(self, true)
|
86
|
+
end
|
87
|
+
|
79
88
|
def resource_class
|
80
89
|
resource.safe_constantize
|
81
90
|
end
|
@@ -86,11 +95,20 @@ class Import < ActiveRecord::Base
|
|
86
95
|
|
87
96
|
##
|
88
97
|
# Override this if you prefer have
|
89
|
-
#
|
98
|
+
# private permissions or you have
|
90
99
|
# private methods for reading files
|
91
100
|
##
|
92
101
|
def import_file
|
93
|
-
local_path? ? file.path : file.url
|
102
|
+
local_path?(file) ? file.path : file.url
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Override this method if you have
|
107
|
+
# private permissions or you have private methods
|
108
|
+
# for reading/writing uploaded files
|
109
|
+
##
|
110
|
+
def failed_file_path
|
111
|
+
local_path?(failed_file) ? failed_file.path : failed_file.url
|
94
112
|
end
|
95
113
|
|
96
114
|
private
|
@@ -100,13 +118,14 @@ class Import < ActiveRecord::Base
|
|
100
118
|
errors.add(:find_options, "can't be blank") if find_options.blank?
|
101
119
|
end
|
102
120
|
|
103
|
-
def local_path?
|
104
|
-
File.exist?
|
121
|
+
def local_path?(f)
|
122
|
+
File.exist? f.path
|
105
123
|
end
|
106
124
|
end
|
107
125
|
```
|
108
126
|
|
109
|
-
Add `acts_as_importable` to
|
127
|
+
### Add `acts_as_importable` to any ActiveRecord model to make it importable
|
128
|
+
|
110
129
|
```ruby
|
111
130
|
class User < ActiveRecord::Base
|
112
131
|
acts_as_importable
|
@@ -120,9 +139,35 @@ class User < ActiveRecord::Base
|
|
120
139
|
last_name: 'dela Cruz' },
|
121
140
|
find_options: %i(email),
|
122
141
|
before_save: Proc.new { |user| user.password = 'temporarypassword123' }
|
142
|
+
after_save: Proc.new { |user| puts "THIS IS CALLED AFTER OBJECT IS SAVED" }
|
123
143
|
end
|
124
144
|
```
|
125
145
|
|
146
|
+
If you're using ActiveRecord::Store, you may import values to your accessors by including them in the configuration:
|
147
|
+
```ruby
|
148
|
+
class User < ActiveRecord::Base
|
149
|
+
store :properties, accessors: [:first_key, :second_key]
|
150
|
+
|
151
|
+
acts_as_importable store_accessors: [:first_key, :second_key]
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
### Add import form
|
156
|
+
This is a sample import HAML form:
|
157
|
+
```ruby
|
158
|
+
# resource is your Model name
|
159
|
+
= f.input :resource
|
160
|
+
# batch_size is useful for large csv file
|
161
|
+
= f.input :batch_size
|
162
|
+
# insert_methods: [:upsert, :insert, :error_on_duplicate]
|
163
|
+
= f.input :insert_method, collection: insert_methods, class: 'form-control insert-method'
|
164
|
+
# `find_options` are the list of columns you want to use to update a certain instance or
|
165
|
+
# error when a duplicate is found. This is not required when your insert_method is `:insert`
|
166
|
+
= f.input :find_options
|
167
|
+
= f.input :file, as: :file,
|
168
|
+
input_html: { accept: '.csv' }
|
169
|
+
```
|
170
|
+
|
126
171
|
You may also add some options from the SmarterCSV gem:
|
127
172
|
|
128
173
|
| Option | Default
|
@@ -135,24 +180,51 @@ You may also add some options from the SmarterCSV gem:
|
|
135
180
|
| :chunk_size | 500
|
136
181
|
| :col_sep | ","
|
137
182
|
|
138
|
-
|
183
|
+
https://github.com/tilo/smarter_csv
|
184
|
+
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
class User < ActiveRecord::Base
|
188
|
+
acts_as_importable csv_opts: {
|
189
|
+
chunk_size: 2000,
|
190
|
+
col_sep: '|',
|
191
|
+
convert_values_to_numeric: { only: [:age, :salary] }
|
192
|
+
}
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
`I'll add more options SOON!`
|
139
197
|
|
140
|
-
|
198
|
+
|
199
|
+
### Create Imports Controller:
|
141
200
|
```ruby
|
142
201
|
class ImportsController < ApplicationController
|
143
202
|
|
144
203
|
def create
|
145
204
|
@import = Import.create!(import_params)
|
146
|
-
@import.
|
147
|
-
@import.execute
|
205
|
+
@import.execute!
|
148
206
|
end
|
149
207
|
|
208
|
+
private
|
209
|
+
|
210
|
+
def import_params
|
211
|
+
params.require(:import).permit(:file, :resource, :insert_method, :batch_size)
|
212
|
+
end
|
150
213
|
end
|
151
214
|
```
|
152
215
|
|
153
|
-
|
154
|
-
|
155
|
-
|
216
|
+
#### Run it via Rails Console:
|
217
|
+
```ruby
|
218
|
+
File.open(PATH_TO_CSV_FILE) do |file|
|
219
|
+
@import = Import.create!(
|
220
|
+
resource: 'User',
|
221
|
+
file: file,
|
222
|
+
insert_method: 'upsert',
|
223
|
+
find_options: 'first_name,last_name'
|
224
|
+
)
|
225
|
+
end
|
226
|
+
@import.execute!
|
227
|
+
```
|
156
228
|
|
157
229
|
|
158
230
|
## Development
|
@@ -169,4 +241,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/michae
|
|
169
241
|
## License
|
170
242
|
|
171
243
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
172
|
-
|
@@ -17,11 +17,15 @@ module ActiveRecordImporter
|
|
17
17
|
autoload :Importable, 'active_record_importer/importable'
|
18
18
|
autoload :InstanceBuilder, 'active_record_importer/instance_builder'
|
19
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
20
|
autoload :TransitionProcessor, 'active_record_importer/transition_processor'
|
23
21
|
autoload :ImportCallbacker, 'active_record_importer/import_callbacker'
|
24
|
-
autoload :
|
22
|
+
autoload :FailedFileBuilder, 'active_record_importer/failed_file_builder'
|
23
|
+
|
24
|
+
module Attribute
|
25
|
+
autoload :AttributesBuilder, 'active_record_importer/attribute/attributes_builder'
|
26
|
+
autoload :FindOptionsBuilder, 'active_record_importer/attribute/find_options_builder'
|
27
|
+
autoload :Helpers, 'active_record_importer/attribute/helpers'
|
28
|
+
end
|
25
29
|
|
26
30
|
require 'active_record_importer/railtie' if defined?(Rails) && Rails::VERSION::MAJOR >= 3
|
27
31
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
module Attribute
|
3
|
+
class AttributesBuilder
|
4
|
+
include Attribute::Helpers
|
5
|
+
|
6
|
+
attr_reader :importable, :row_attrs, :processed_attrs
|
7
|
+
|
8
|
+
delegate :importer_options, to: :importable
|
9
|
+
delegate :importable_columns,
|
10
|
+
:default_attributes,
|
11
|
+
:find_assoc_opts,
|
12
|
+
to: :importer_options
|
13
|
+
|
14
|
+
def initialize(importable, row_attrs)
|
15
|
+
@importable = importable
|
16
|
+
@row_attrs = row_attrs
|
17
|
+
@processed_attrs = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def build
|
21
|
+
force_encode_attributes
|
22
|
+
fetch_time_attributes
|
23
|
+
processed_attrs
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def default_attrs
|
29
|
+
def_attrs = { importing: true }
|
30
|
+
default_attributes.each do |key, value|
|
31
|
+
def_attrs[key] = fetch_value(value)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def force_encode_attributes
|
36
|
+
@processed_attrs = force_utf8_encode(merged_attributes)
|
37
|
+
end
|
38
|
+
|
39
|
+
def fetch_time_attributes
|
40
|
+
@processed_attrs.merge!(time_attributes(processed_attrs))
|
41
|
+
end
|
42
|
+
|
43
|
+
def fetch_value(value)
|
44
|
+
case value
|
45
|
+
when Proc
|
46
|
+
value.call(row_attrs)
|
47
|
+
when Symbol
|
48
|
+
importable.send(value, row_attrs)
|
49
|
+
else
|
50
|
+
value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def merged_attributes
|
55
|
+
attributes = row_attrs.slice(*importable_columns)
|
56
|
+
row_attrs = attributes.inject({}) do |row_attrs, key_value|
|
57
|
+
row_attrs[key_value.first] = key_value.last if key_value.last.present?
|
58
|
+
row_attrs
|
59
|
+
end
|
60
|
+
default_attrs.merge(row_attrs)
|
61
|
+
end
|
62
|
+
|
63
|
+
def has_column?(column)
|
64
|
+
return if column.blank?
|
65
|
+
importable_columns.include?(column.to_sym)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
module Attribute
|
3
|
+
class FindOptionsBuilder
|
4
|
+
include Virtus.model
|
5
|
+
include Helpers
|
6
|
+
|
7
|
+
attribute :resource, String
|
8
|
+
attribute :attrs, Hash, default: {}
|
9
|
+
attribute :find_options, String
|
10
|
+
attribute :prefix, String
|
11
|
+
|
12
|
+
def build
|
13
|
+
get_find_opts
|
14
|
+
slice_attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def klass
|
20
|
+
resource.safe_constantize
|
21
|
+
end
|
22
|
+
|
23
|
+
delegate :importer_options, to: :klass
|
24
|
+
|
25
|
+
delegate :required_attributes, to: :importer_options
|
26
|
+
|
27
|
+
def get_find_opts
|
28
|
+
@options = strip_and_symbolize
|
29
|
+
@options ||= importer_options.find_options || required_attributes
|
30
|
+
@options
|
31
|
+
end
|
32
|
+
|
33
|
+
def slice_attributes
|
34
|
+
return attrs.slice(*@options).compact if prefix.blank?
|
35
|
+
|
36
|
+
@options.inject({}) do |attr, key|
|
37
|
+
attr[key] = attrs[prefixed_key(key)].presence
|
38
|
+
attr
|
39
|
+
end.compact
|
40
|
+
end
|
41
|
+
|
42
|
+
def prefixed_key(key)
|
43
|
+
"#{prefix}#{key}".to_sym
|
44
|
+
end
|
45
|
+
|
46
|
+
def strip_and_symbolize
|
47
|
+
return if find_options.blank?
|
48
|
+
find_options.split(',').map do |key|
|
49
|
+
key.strip.to_sym
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
module Attribute
|
3
|
+
module Helpers
|
4
|
+
|
5
|
+
def parse_datetime(datetime = nil)
|
6
|
+
return if datetime.blank?
|
7
|
+
Time.parse(datetime)
|
8
|
+
end
|
9
|
+
|
10
|
+
def force_utf8_encode(data = {})
|
11
|
+
return data if data.blank?
|
12
|
+
|
13
|
+
data.keys.each do |key|
|
14
|
+
data[key] = data[key].force_encoding('UTF-8') if data[key].is_a?(String)
|
15
|
+
end
|
16
|
+
|
17
|
+
data
|
18
|
+
end
|
19
|
+
|
20
|
+
def time_attributes(data = {})
|
21
|
+
attrs = {}
|
22
|
+
attrs[:created_at] = parse_datetime(data[:created_at]) || Time.now
|
23
|
+
attrs[:updated_at] = parse_datetime(data[:updated_at]) || attrs[:created_at]
|
24
|
+
attrs
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -1,32 +1,44 @@
|
|
1
1
|
module ActiveRecordImporter
|
2
2
|
class BatchImporter
|
3
3
|
|
4
|
-
attr_reader :data, :import
|
4
|
+
attr_reader :data, :import, :failed_file, :processor
|
5
5
|
|
6
6
|
def initialize(import, data)
|
7
7
|
@import = import
|
8
8
|
@data = data
|
9
|
+
@failed_file = FailedFileBuilder.new(import)
|
9
10
|
end
|
10
11
|
|
11
12
|
def process!
|
12
13
|
@imported_count, @failed_count = 0, 0
|
13
|
-
|
14
14
|
data.each do |row|
|
15
15
|
next if row.blank?
|
16
|
-
|
17
|
-
processor.process ? @imported_count += 1 : @failed_count += 1
|
16
|
+
process_row(row.symbolize_keys!)
|
18
17
|
end
|
19
18
|
|
20
19
|
set_import_count
|
20
|
+
finalize_batch_import
|
21
21
|
end
|
22
22
|
|
23
23
|
private
|
24
24
|
|
25
|
+
def process_row(row)
|
26
|
+
processor = DataProcessor.new(import, row)
|
27
|
+
return @imported_count += 1 if processor.process
|
28
|
+
|
29
|
+
@failed_file.failed_rows << row.merge(import_errors: processor.row_errors)
|
30
|
+
@failed_count += 1
|
31
|
+
end
|
32
|
+
|
25
33
|
def set_import_count
|
26
34
|
Import.update_counters(import.id, imported_rows: @imported_count)
|
27
35
|
Import.update_counters(import.id, failed_rows: @failed_count)
|
28
36
|
end
|
29
37
|
|
38
|
+
def finalize_batch_import
|
39
|
+
@failed_file.build
|
40
|
+
end
|
41
|
+
|
30
42
|
def importable
|
31
43
|
import.resource.safe_constantize
|
32
44
|
end
|
@@ -38,7 +38,7 @@ module ActiveRecordImporter
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def fetch_instance_attributes
|
41
|
-
@attributes = AttributesBuilder.new(
|
41
|
+
@attributes = Attribute::AttributesBuilder.new(
|
42
42
|
importable, row_attrs
|
43
43
|
).build
|
44
44
|
rescue => exception
|
@@ -46,7 +46,7 @@ module ActiveRecordImporter
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def fetch_find_attributes
|
49
|
-
@find_attributes = FindOptionsBuilder.new(
|
49
|
+
@find_attributes = Attribute::FindOptionsBuilder.new(
|
50
50
|
resource: import.resource,
|
51
51
|
find_options: import.find_options,
|
52
52
|
attrs: attributes
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module ActiveRecordImporter
|
2
|
+
class FailedFileBuilder
|
3
|
+
attr_reader :import
|
4
|
+
attr_accessor :failed_rows
|
5
|
+
|
6
|
+
def initialize(import)
|
7
|
+
@import = import
|
8
|
+
@failed_rows = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def build
|
12
|
+
return if failed_rows.blank?
|
13
|
+
|
14
|
+
create_or_append_to_csv
|
15
|
+
create_import_failed_file
|
16
|
+
destroy_temp_file
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def create_or_append_to_csv
|
22
|
+
puts 'TEST!!!'
|
23
|
+
if import.failed_file.present?
|
24
|
+
puts 'APPEND!!!!'
|
25
|
+
append_rows_to_file
|
26
|
+
else
|
27
|
+
puts 'WRITE!!!!'
|
28
|
+
write_csv_file
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def write_csv_file
|
33
|
+
CSV.open(temp_file_path, 'wb') do |csv|
|
34
|
+
csv << failed_rows.first.keys
|
35
|
+
insert_failed_rows(csv)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def append_rows_to_file
|
40
|
+
return if import.failed_file.blank?
|
41
|
+
CSV.open(import.failed_file_path, 'a+') do |csv|
|
42
|
+
insert_failed_rows(csv)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
def insert_failed_rows(csv)
|
48
|
+
failed_rows.each do |hash|
|
49
|
+
csv << hash.values
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def destroy_temp_file
|
54
|
+
return unless File.exists?(temp_file_path)
|
55
|
+
FileUtils.rm(temp_file_path)
|
56
|
+
end
|
57
|
+
|
58
|
+
def temp_file_path
|
59
|
+
"/tmp/#{target_file_name}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def target_file_name
|
63
|
+
"failed_file_#{import.id}.csv"
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_import_failed_file
|
67
|
+
return if import.failed_file.present?
|
68
|
+
File.open(temp_file_path) do |file|
|
69
|
+
import.failed_file = file
|
70
|
+
|
71
|
+
# I forced to save it as 'text/csv' because
|
72
|
+
# the file is being saved as 'text/x-pascal'
|
73
|
+
# and I still have no idea why?!?
|
74
|
+
|
75
|
+
import.failed_file_content_type = 'text/csv'
|
76
|
+
import.save!
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/app/models/import.rb
CHANGED
@@ -8,6 +8,7 @@ module ActiveRecordImporter
|
|
8
8
|
default: :upsert
|
9
9
|
|
10
10
|
has_attached_file :file
|
11
|
+
has_attached_file :failed_file
|
11
12
|
|
12
13
|
attr_accessor :execute_on_create
|
13
14
|
|
@@ -18,15 +19,22 @@ module ActiveRecordImporter
|
|
18
19
|
content_type: %w(text/plain text/csv)
|
19
20
|
}
|
20
21
|
|
21
|
-
|
22
|
+
validates_attachment :failed_file,
|
23
|
+
content_type: {
|
24
|
+
content_type: %w(text/plain text/csv)
|
25
|
+
}
|
22
26
|
|
23
|
-
# I'll add import options in the next release
|
27
|
+
# I'll add import options in the next major release
|
24
28
|
# accepts_nested_attributes_for :import_options, allow_destroy: true
|
25
29
|
|
26
30
|
def execute
|
27
31
|
resource_class.import!(self, execute_on_create)
|
28
32
|
end
|
29
33
|
|
34
|
+
def execute!
|
35
|
+
resource_class.import!(self, true)
|
36
|
+
end
|
37
|
+
|
30
38
|
def resource_class
|
31
39
|
resource.safe_constantize
|
32
40
|
end
|
@@ -37,11 +45,20 @@ module ActiveRecordImporter
|
|
37
45
|
|
38
46
|
##
|
39
47
|
# Override this if you prefer have
|
40
|
-
#
|
48
|
+
# private permissions or you have
|
41
49
|
# private methods for reading files
|
42
50
|
##
|
43
51
|
def import_file
|
44
|
-
local_path? ? file.path : file.url
|
52
|
+
local_path?(file) ? file.path : file.url
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Override this method if you have
|
57
|
+
# private permissions or you have private methods
|
58
|
+
# for reading/writing uploaded files
|
59
|
+
##
|
60
|
+
def failed_file_path
|
61
|
+
local_path?(failed_file) ? failed_file.path : failed_file.url
|
45
62
|
end
|
46
63
|
|
47
64
|
private
|
@@ -51,8 +68,8 @@ module ActiveRecordImporter
|
|
51
68
|
errors.add(:find_options, "can't be blank") if find_options.blank?
|
52
69
|
end
|
53
70
|
|
54
|
-
def local_path?
|
55
|
-
File.exist?
|
71
|
+
def local_path?(f)
|
72
|
+
File.exist? f.path
|
56
73
|
end
|
57
74
|
end
|
58
75
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record_importer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Nera
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-02-
|
11
|
+
date: 2017-02-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -152,14 +152,15 @@ files:
|
|
152
152
|
- bin/setup
|
153
153
|
- db/migrate/1_active_record_importer_migration.rb
|
154
154
|
- lib/active_record_importer.rb
|
155
|
-
- lib/active_record_importer/attributes_builder.rb
|
155
|
+
- lib/active_record_importer/attribute/attributes_builder.rb
|
156
|
+
- lib/active_record_importer/attribute/find_options_builder.rb
|
157
|
+
- lib/active_record_importer/attribute/helpers.rb
|
156
158
|
- lib/active_record_importer/batch_importer.rb
|
157
159
|
- lib/active_record_importer/data_processor.rb
|
158
160
|
- lib/active_record_importer/dispatcher.rb
|
159
161
|
- lib/active_record_importer/engine.rb
|
160
162
|
- lib/active_record_importer/errors.rb
|
161
|
-
- lib/active_record_importer/
|
162
|
-
- lib/active_record_importer/helpers.rb
|
163
|
+
- lib/active_record_importer/failed_file_builder.rb
|
163
164
|
- lib/active_record_importer/import_callbacker.rb
|
164
165
|
- lib/active_record_importer/importable.rb
|
165
166
|
- lib/active_record_importer/instance_builder.rb
|
@@ -1,67 +0,0 @@
|
|
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
|
@@ -1,52 +0,0 @@
|
|
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
|
@@ -1,26 +0,0 @@
|
|
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
|