active_record_importer 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 79df869df341582d9485f48a0798f878ab6047ef
4
- data.tar.gz: 9dd7ced4d90dda9744410144012ece81b8ee4f01
3
+ metadata.gz: 0b0c2019e863558605d2703fb80890c8ba209192
4
+ data.tar.gz: 480be20d7fc0440427e4107f7b50e7e19572ad81
5
5
  SHA512:
6
- metadata.gz: ef41b94191496fd7b9dc37366f87a42ef9f8646a4724d1ce53fa996b11c1337753bad72059152816936961863061c340b05bf114c7e78e97064fd88cbae1c652
7
- data.tar.gz: dd3ffa7d4bedc5b2bcd4b1f33659f07ae9d560b6bf2835b1e9d7e3377943770f24cc5c56490ac804bcb7991f98789fef0c30983da67b03f363f30206aecf7793
6
+ metadata.gz: f9191b603ceb3e77bff828ac70ad0a9301b854a1956cc1ef218e00177c2f8d5b20b0722e98bcc99cb9f99663126e29e53d3b57eebe60d14c88eb248008092441
7
+ data.tar.gz: 4be44dcf15fcd9a9981ff0942c63a1a1e6d21691cf27832cb3d3755a78636f4fe79193fbe26eb5bb4ccc437cbd0e6c0ab5583d73e37f25e67e78b1a8d7d53265
data/README.md CHANGED
@@ -25,8 +25,32 @@ Or install it yourself as:
25
25
  $ gem install active_record_importer
26
26
 
27
27
  ## Usage
28
+ ### For version 0.3.0
28
29
 
29
- Simple usage for now:
30
+ For the newest version (0.3.0), you don't have to create Import table/model and controller.
31
+ You just need to add the `acts_as_importable` in your model you want to be importable, and you may now run:
32
+
33
+ ```ruby
34
+ User.import!(file: File.open(PATH_TO_FILE))
35
+ ```
36
+
37
+ `insert` will be the default insert method for this
38
+ If you want to use `upsert` or `error_duplicate`, define it in your importer options:
39
+
40
+ ```ruby
41
+ class User < ActiveRecord::Base
42
+ acts_as_importable insert_method: 'upsert',
43
+ find_options: [:email]
44
+ end
45
+ ```
46
+
47
+ Or you may use in your console:
48
+
49
+ ```ruby
50
+ User.acts_as_importable insert_method: 'error_duplicate', find_options: ['email']
51
+ ```
52
+
53
+ ### If you don't want to record the status of your import, you don't have to do the remaining steps
30
54
 
31
55
  ### Create Import table/model
32
56
  I'll add a generator on my next release
@@ -76,15 +100,27 @@ class Import < ActiveRecord::Base
76
100
 
77
101
  # I'll add import options in the next major release
78
102
  # accepts_nested_attributes_for :import_options, allow_destroy: true
79
-
103
+ ### THIS IS VERSION 0.2.1 and below
80
104
  def execute
81
105
  resource_class.import!(self, execute_on_create)
82
106
  end
83
107
 
108
+ ### THIS IS VERSION 0.2.1 and below
109
+ def execute
110
+ resource_class.import!(object: self, execute: execute_on_create)
111
+ end
112
+
113
+
114
+ ### THIS IS VERSION 0.2.1 and below
84
115
  def execute!
85
116
  resource_class.import!(self, true)
86
117
  end
87
118
 
119
+ ### THIS IS VERSION 0.3.0
120
+ def execute!
121
+ resource_class.import!(object: self, execute: true)
122
+ end
123
+
88
124
  def resource_class
89
125
  resource.safe_constantize
90
126
  end
@@ -226,6 +262,9 @@ end
226
262
  @import.execute!
227
263
  ```
228
264
 
265
+ ###REMINDER:
266
+ Headers of your csv file should be formatted/transformed to column names of your IMPORTABLE model
267
+
229
268
 
230
269
  ## Development
231
270
 
@@ -4,7 +4,7 @@ module ActiveRecordImporter
4
4
  include Virtus.model
5
5
  include Helpers
6
6
 
7
- attribute :resource, String
7
+ attribute :importable
8
8
  attribute :attrs, Hash, default: {}
9
9
  attribute :find_options, String
10
10
  attribute :prefix, String
@@ -16,18 +16,8 @@ module ActiveRecordImporter
16
16
 
17
17
  private
18
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
19
  def get_find_opts
28
20
  @options = strip_and_symbolize
29
- @options ||= importer_options.find_options || required_attributes
30
- @options
31
21
  end
32
22
 
33
23
  def slice_attributes
@@ -1,19 +1,18 @@
1
1
  module ActiveRecordImporter
2
2
  class BatchImporter
3
+ include Virtus.model
3
4
 
4
- attr_reader :data, :import, :failed_file, :processor
5
-
6
- def initialize(import, data)
7
- @import = import
8
- @data = data
9
- @failed_file = FailedFileBuilder.new(import)
10
- end
5
+ attribute :import, Import
6
+ attribute :importable
7
+ attribute :data, Array, default: []
8
+ attribute :failed_file, FailedFileBuilder, default: :initialize_failed_file
11
9
 
12
10
  def process!
13
11
  @imported_count, @failed_count = 0, 0
14
- data.each do |row|
15
- next if row.blank?
16
- process_row(row.symbolize_keys!)
12
+
13
+ data.each do |row_attrs|
14
+ next if row_attrs.blank?
15
+ process_row(row_attrs.symbolize_keys!)
17
16
  end
18
17
 
19
18
  set_import_count
@@ -22,25 +21,39 @@ module ActiveRecordImporter
22
21
 
23
22
  private
24
23
 
25
- def process_row(row)
26
- processor = DataProcessor.new(import, row)
24
+ def initialize_failed_file
25
+ return unless import
26
+ FailedFileBuilder.new(import)
27
+ end
28
+
29
+ def process_row(row_attrs)
30
+ processor =
31
+ DataProcessor.new(
32
+ import: import,
33
+ importable: importable,
34
+ row_attrs: row_attrs
35
+ )
27
36
  return @imported_count += 1 if processor.process
28
37
 
29
- @failed_file.failed_rows << row.merge(import_errors: processor.row_errors)
38
+ collect_failed_rows(row_attrs, processor.row_errors)
30
39
  @failed_count += 1
31
40
  end
32
41
 
33
42
  def set_import_count
43
+ return unless import
44
+
34
45
  Import.update_counters(import.id, imported_rows: @imported_count)
35
46
  Import.update_counters(import.id, failed_rows: @failed_count)
36
47
  end
37
48
 
38
- def finalize_batch_import
39
- @failed_file.build
49
+ def collect_failed_rows(row_attrs, errors)
50
+ return puts errors.inspect unless failed_file
51
+ @failed_file.failed_rows << row_attrs.merge(import_errors: errors)
40
52
  end
41
53
 
42
- def importable
43
- import.resource.safe_constantize
54
+ def finalize_batch_import
55
+ return unless failed_file
56
+ @failed_file.build
44
57
  end
45
58
 
46
59
  delegate :importer_options, to: :importable
@@ -1,16 +1,16 @@
1
1
  module ActiveRecordImporter
2
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
3
+ include Virtus.model
4
+
5
+ attribute :import, Import
6
+ attribute :importable, Class
7
+ attribute :insert_method, String, default: :set_insert_method
8
+ attribute :row_attrs, Hash
9
+ attribute :instance_attrs, Hash
10
+ attribute :find_options, String, default: :set_find_options
11
+ attribute :find_attributes, Hash
12
+ attribute :row_errors, Array
13
+ attribute :instance
14
14
 
15
15
  def process
16
16
  fetch_instance_attributes
@@ -24,10 +24,12 @@ module ActiveRecordImporter
24
24
  ActiveRecord::Base.transaction do
25
25
  begin
26
26
  @instance =
27
- InstanceBuilder.new(
28
- import, find_attributes,
29
- attributes_without_state_machine_attrs
30
- ).build
27
+ InstanceBuilder.new(
28
+ importable: importable,
29
+ insert_method: insert_method,
30
+ find_attributes: find_attributes,
31
+ instance_attrs: attributes_without_state_machine_attrs
32
+ ).build
31
33
 
32
34
  methods_after_upsert
33
35
  true
@@ -38,18 +40,18 @@ module ActiveRecordImporter
38
40
  end
39
41
 
40
42
  def fetch_instance_attributes
41
- @attributes = Attribute::AttributesBuilder.new(
42
- importable, row_attrs
43
- ).build
43
+ @instance_attrs = Attribute::AttributesBuilder.new(
44
+ importable, row_attrs
45
+ ).build
44
46
  rescue => exception
45
47
  append_errors(exception)
46
48
  end
47
49
 
48
50
  def fetch_find_attributes
49
51
  @find_attributes = Attribute::FindOptionsBuilder.new(
50
- resource: import.resource,
51
- find_options: import.find_options,
52
- attrs: attributes
52
+ importable: importable,
53
+ find_options: find_options,
54
+ attrs: instance_attrs
53
55
  ).build
54
56
  rescue => exception
55
57
  append_errors(exception)
@@ -62,8 +64,10 @@ module ActiveRecordImporter
62
64
  run_after_save_callbacks
63
65
  end
64
66
 
65
- delegate :after_save,
66
- :state_machine_attr,
67
+ delegate :importer_options,
68
+ to: :importable
69
+
70
+ delegate :after_save, :state_machine_attr,
67
71
  to: :importer_options
68
72
 
69
73
  def state_transitions
@@ -77,7 +81,7 @@ module ActiveRecordImporter
77
81
  end
78
82
 
79
83
  def attributes_without_state_machine_attrs
80
- attributes.except(*state_machine_attr)
84
+ instance_attrs.except(*state_machine_attr)
81
85
  end
82
86
 
83
87
  def skip_callbacks?
@@ -97,5 +101,15 @@ module ActiveRecordImporter
97
101
  @row_errors = message
98
102
  fail ActiveRecord::Rollback if rollback
99
103
  end
104
+
105
+ def set_insert_method
106
+ @insert_method = import.try(:insert_method)
107
+ @insert_method ||= importer_options.insert_method
108
+ @insert_method ||= 'insert'
109
+ end
110
+
111
+ def set_find_options
112
+ import.try(:find_options) || importer_options.find_options.join(',')
113
+ end
100
114
  end
101
115
  end
@@ -1,11 +1,11 @@
1
1
  module ActiveRecordImporter
2
2
  class Dispatcher
3
- attr_reader :import, :execute
3
+ include Virtus.model
4
4
 
5
- def initialize(import_id, execute = true)
6
- @import = Import.find(import_id)
7
- @execute = execute
8
- end
5
+ attribute :import, Import
6
+ attribute :importable, Class
7
+ attribute :execute, Boolean, default: true
8
+ attribute :import_file
9
9
 
10
10
  def call
11
11
  divide_and_conquer
@@ -14,22 +14,22 @@ module ActiveRecordImporter
14
14
  private
15
15
 
16
16
  def divide_and_conquer
17
- File.open(import.import_file, 'r:bom|utf-8') do |file|
17
+ File.open(import_file, 'r:bom|utf-8') do |file|
18
18
  SmarterCSV.process(file, csv_options) do |collection|
19
19
  queue_or_execute(collection)
20
20
  end
21
21
  end
22
+ true
22
23
  end
23
24
 
24
25
  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
26
+ opts = klass_csv_opts
27
+ return opts if import.nil? || import.batch_size.blank?
28
28
  opts.merge(chunk_size: import.batch_size)
29
29
  end
30
30
 
31
- def klass_csv_opts(klass)
32
- klass.importer_options.csv_opts.to_hash
31
+ def klass_csv_opts
32
+ importable.importer_options.csv_opts.to_hash
33
33
  end
34
34
 
35
35
  def queue_or_execute(collection)
@@ -38,7 +38,11 @@ module ActiveRecordImporter
38
38
  end
39
39
 
40
40
  def process_import(collection)
41
- BatchImporter.new(import, collection).process!
41
+ BatchImporter.new(
42
+ import: import,
43
+ importable: importable,
44
+ data: collection
45
+ ).process!
42
46
  end
43
47
 
44
48
  def queue(collection)
@@ -17,5 +17,11 @@ module ActiveRecordImporter
17
17
  super 'Duplicate record found!'
18
18
  end
19
19
  end
20
+
21
+ class MissingImportFile < StandardError
22
+ def initialize
23
+ super 'File is missing for import'
24
+ end
25
+ end
20
26
  end
21
27
  end
@@ -6,6 +6,8 @@ module ActiveRecordImporter
6
6
 
7
7
  module ClassMethods
8
8
  ##
9
+ # #acts_as_importable
10
+ #
9
11
  # Make a model importable
10
12
  # This will allow a model to use the importer
11
13
  #
@@ -29,13 +31,60 @@ module ActiveRecordImporter
29
31
  @@importer_options
30
32
  end
31
33
 
32
- def import!(import_object, execute = true)
33
- ActiveRecordImporter::Dispatcher.new(
34
- import_object.id, execute).call
34
+ def importable?
35
+ ::IMPORTABLES.include?(self.name)
36
+ end
37
+
38
+ ##
39
+ # #import!
40
+ #
41
+ # This method is called in the Import instance during execution of import
42
+ # You may also call this method without any import instance
43
+ # e.g.
44
+ #
45
+ # User.import!(file: File.open(PATH_TO_FILE))
46
+ #
47
+ # "insert" will be the default insert method for this
48
+ # If you want to use "upsert" or "error_duplicate",
49
+ # define it in your importer options:
50
+ #
51
+ # class User < ActiveRecord::Base
52
+ # acts_as_importable insert_method: 'upsert',
53
+ # find_options: ['email']
54
+ # end
55
+ #
56
+ # Or you may use:
57
+ # User.acts_as_importable insert_method: 'error_duplicate', find_options: ['email']
58
+ #
59
+ ##
60
+ def import!(options = {})
61
+ fail "#{self.name} is not importable" unless importable?
62
+
63
+ import_object = options.fetch(:object, nil)
64
+ execute = options.fetch(:execute, true)
65
+ import_file = get_import_file(import_object, options)
66
+
67
+ call_dispatcher(import_object, execute, import_file)
35
68
  end
36
69
 
37
70
  private
38
71
 
72
+ def call_dispatcher(import_object = nil, execute = true, file = nil)
73
+ ActiveRecordImporter::Dispatcher.new(
74
+ importable: self,
75
+ import: import_object,
76
+ execute: execute,
77
+ import_file: file
78
+ ).call
79
+ end
80
+
81
+ def get_import_file(import, options = {})
82
+ file = options.fetch(:file, nil) || import.try(:import_file)
83
+ fail Errors::MissingImportFile.new unless file
84
+ file
85
+ end
86
+
87
+
39
88
  def allowed_columns_hash(options = {})
40
89
  {
41
90
  importable_columns: allowed_columns(options)
@@ -1,12 +1,11 @@
1
1
  module ActiveRecordImporter
2
2
  class InstanceBuilder
3
- attr_reader :attributes, :find_attributes, :import
3
+ include Virtus.model
4
4
 
5
- def initialize(import, find_attributes, attributes)
6
- @import = import
7
- @find_attributes = find_attributes
8
- @attributes = attributes
9
- end
5
+ attribute :importable, ActiveRecord::Base
6
+ attribute :insert_method, String, default: 'insert'
7
+ attribute :instance_attrs, Hash, default: {}
8
+ attribute :find_attributes, Hash, default: {}
10
9
 
11
10
  def build
12
11
  instance = initialize_instance
@@ -15,20 +14,16 @@ module ActiveRecordImporter
15
14
 
16
15
  private
17
16
 
18
- delegate :insert_method, to: :import
17
+ delegate :error_duplicate, :insert?, to: :insert_method_inquiry
19
18
 
20
19
  def initialize_instance
21
- return klass.new if insert_method.insert?
20
+ return importable.new if insert?
22
21
 
23
22
  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
23
+ importable.find_or_initialize_by(find_attributes)
29
24
  end
30
25
 
31
- delegate :importer_options, to: :klass
26
+ delegate :importer_options, to: :importable
32
27
  delegate :before_save, to: :importer_options
33
28
 
34
29
  def process_data(instance)
@@ -44,13 +39,17 @@ module ActiveRecordImporter
44
39
  end
45
40
 
46
41
  def assign_attrs_and_save!(instance)
47
- instance.attributes = attributes
42
+ instance.attributes = instance_attrs
48
43
  instance.save!
49
44
  instance
50
45
  end
51
46
 
47
+ def insert_method_inquiry
48
+ insert_method.inquiry
49
+ end
50
+
52
51
  def error_duplicate?(instance)
53
- instance.persisted? && insert_method.error_duplicate?
52
+ instance.persisted? && error_duplicate?
54
53
  end
55
54
  end
56
55
  end
@@ -31,9 +31,10 @@ module ActiveRecordImporter
31
31
  class ImporterOptions
32
32
  include Virtus.model
33
33
 
34
- attribute :find_options, Array
34
+ attribute :find_options, Array, default: []
35
35
  attribute :exclude_from_find_options, Array
36
36
  attribute :scope, Symbol
37
+ attribute :insert_method, String
37
38
  attribute :importable_columns, Array
38
39
  attribute :default_attributes, Hash
39
40
  attribute :csv_opts, CsvOptions, default: CsvOptions.new
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordImporter
2
- VERSION = '0.2.1'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -28,11 +28,11 @@ module ActiveRecordImporter
28
28
  # accepts_nested_attributes_for :import_options, allow_destroy: true
29
29
 
30
30
  def execute
31
- resource_class.import!(self, execute_on_create)
31
+ resource_class.import!(object: self, execute: execute_on_create)
32
32
  end
33
33
 
34
34
  def execute!
35
- resource_class.import!(self, true)
35
+ resource_class.import!(object: self, execute: true)
36
36
  end
37
37
 
38
38
  def resource_class
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_importer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Nera