active_record_importer 0.1.0 → 0.2.0
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.
- 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
|