activerecord_csv_importer 0.1.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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.rubocop.yml +20 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/Guardfile +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +339 -0
- data/Rakefile +6 -0
- data/activerecord_csv_importer.gemspec +34 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/activerecord_csv_importer/column.rb +10 -0
- data/lib/activerecord_csv_importer/column_definition.rb +67 -0
- data/lib/activerecord_csv_importer/config.rb +16 -0
- data/lib/activerecord_csv_importer/csv_reader.rb +74 -0
- data/lib/activerecord_csv_importer/dsl.rb +25 -0
- data/lib/activerecord_csv_importer/header.rb +57 -0
- data/lib/activerecord_csv_importer/report.rb +92 -0
- data/lib/activerecord_csv_importer/report_message.rb +44 -0
- data/lib/activerecord_csv_importer/row.rb +40 -0
- data/lib/activerecord_csv_importer/runner.rb +66 -0
- data/lib/activerecord_csv_importer/version.rb +3 -0
- data/lib/activerecord_csv_importer.rb +90 -0
- metadata +180 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 62b46ebbaa1845d396dba93a645a0e380634c0f5
|
4
|
+
data.tar.gz: 6b3487666658e35b4b195bce03043165b1788675
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 044fab5881d472092998ea2ca7c893b86ac1ff1671b8380ed94462793b93b7088e22e90c1a3f7df5b5dbe69d192c8690c5edcf7aa6a3d9d1be2475af42ee1a2d
|
7
|
+
data.tar.gz: 5be82ce068a1f0651aca75317dbe094d372ad6ee34730cada3cdccd8ddcec95d2fca170a7e54eb4e2066479f961862aefd62f2eae2ffc4417395dd14e38be7b9
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Style/Documentation:
|
2
|
+
Enabled: false
|
3
|
+
|
4
|
+
Metrics/LineLength:
|
5
|
+
Max: 100
|
6
|
+
|
7
|
+
Metrics/MethodLength:
|
8
|
+
Max: 15
|
9
|
+
|
10
|
+
Style/BlockDelimiters:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
Style/AsciiComments:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
Style/StructInheritance:
|
17
|
+
Enabled: false
|
18
|
+
|
19
|
+
Lint/HandleExceptions:
|
20
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
group :red_green_refactor, halt_on_fail: true do
|
2
|
+
guard :rspec, cmd: 'rspec' do
|
3
|
+
require 'guard/rspec/dsl'
|
4
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
5
|
+
|
6
|
+
# Feel free to open issues for suggestions and improvements
|
7
|
+
|
8
|
+
# RSpec files
|
9
|
+
rspec = dsl.rspec
|
10
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
11
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
12
|
+
watch(rspec.spec_files)
|
13
|
+
|
14
|
+
# Ruby files
|
15
|
+
ruby = dsl.ruby
|
16
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
17
|
+
end
|
18
|
+
|
19
|
+
guard :rubocop, all_on_start: false, cmd: 'rubocop', cli: '--format fuubar' do
|
20
|
+
watch(/.+\.rb/)
|
21
|
+
watch(%r{(?:.+/)?\.rubocop\.yml$}) { |m| File.dirname(m[0]) }
|
22
|
+
end
|
23
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Zulfiqar Ali
|
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,339 @@
|
|
1
|
+
# ActiveRecordCSVImporter
|
2
|
+
|
3
|
+
Using bulk inserts, the goal for ActiveRecordCSVImporter is to speed up the processing of large CSV filed
|
4
|
+
|
5
|
+
## Differences from CSVImporter
|
6
|
+
|
7
|
+
The key difference with ActiveRecordCSVImporter is the removal of callback support, since that is incompatible with bulk inserts.
|
8
|
+
Additional config options are available instead to deal with batching and duplicate index support.
|
9
|
+
|
10
|
+
It is compatible with ActiveRecord and any Databases supported by activerecord-import.
|
11
|
+
|
12
|
+
## Usage tldr;
|
13
|
+
|
14
|
+
Define your CSVImporter:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
class ImportUserCSV
|
18
|
+
include ActiveRecordCSVImporter
|
19
|
+
|
20
|
+
model User # an active record like model
|
21
|
+
|
22
|
+
column :email, to: ->(email) { email.downcase }, required: true
|
23
|
+
column :first_name, as: [ /first.?name/i, /pr(é|e)nom/i ]
|
24
|
+
column :last_name, as: [ /last.?name/i, "nom" ]
|
25
|
+
|
26
|
+
on_duplicate_key(
|
27
|
+
on_duplicate_key_update: {
|
28
|
+
conflict_target: [:email], columns: [:first_name, :last_name]
|
29
|
+
}
|
30
|
+
)
|
31
|
+
|
32
|
+
batch_size 500
|
33
|
+
|
34
|
+
each_batch { |report|
|
35
|
+
puts report.total_count
|
36
|
+
puts report.completed_count
|
37
|
+
puts report.failed_rows
|
38
|
+
}
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
Run the import:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
import = ImportUserCSV.new(file: my_file)
|
46
|
+
|
47
|
+
import.valid_header? # => false
|
48
|
+
import.report.message # => "The following columns are required: email"
|
49
|
+
|
50
|
+
# Assuming the header was valid, let's run the import!
|
51
|
+
|
52
|
+
import.run!
|
53
|
+
import.report.sucess? # => true
|
54
|
+
import.report.message # => "Import completed. 4 created, 2 updated, 1 failed to update"
|
55
|
+
```
|
56
|
+
|
57
|
+
## Installation
|
58
|
+
|
59
|
+
Add this line to your application's Gemfile:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
gem 'activerecord_csv_importer'
|
63
|
+
```
|
64
|
+
|
65
|
+
And then execute:
|
66
|
+
|
67
|
+
$ bundle
|
68
|
+
|
69
|
+
Or install it yourself as:
|
70
|
+
|
71
|
+
$ gem activerecord_csv_importer
|
72
|
+
|
73
|
+
## Usage
|
74
|
+
|
75
|
+
### Create an Importer
|
76
|
+
|
77
|
+
Create a class and include `CSVImporter`.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
class ImportUserCSV
|
81
|
+
include CSVImporter
|
82
|
+
end
|
83
|
+
```
|
84
|
+
|
85
|
+
### Associate an active record model
|
86
|
+
|
87
|
+
The `model` is can be a active record model.
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
class ImportUserCSV
|
91
|
+
include CSVImporter
|
92
|
+
|
93
|
+
model User
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
It can also be a relation which is handy to preset attributes:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
class User
|
101
|
+
scope :pending, -> { where(status: 'pending') }
|
102
|
+
end
|
103
|
+
|
104
|
+
class ImportUserCSV
|
105
|
+
include CSVImporter
|
106
|
+
|
107
|
+
model User.pending
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
You can change the configuration at runtime to scope down to associated
|
112
|
+
records.
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
class Team
|
116
|
+
has_many :users
|
117
|
+
end
|
118
|
+
|
119
|
+
team = Team.find(1)
|
120
|
+
|
121
|
+
ImportUserCSV.new(path: "tmp/my_file.csv") do
|
122
|
+
model team.users
|
123
|
+
end
|
124
|
+
```
|
125
|
+
|
126
|
+
|
127
|
+
### Define columns and their mapping
|
128
|
+
|
129
|
+
This is where the fun begins.
|
130
|
+
|
131
|
+
```ruby
|
132
|
+
class ImportUserCSV
|
133
|
+
include CSVImporter
|
134
|
+
|
135
|
+
model User
|
136
|
+
|
137
|
+
column :email
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
This will map the column named email to the email attribute. By default,
|
142
|
+
we downcase and strip the columns so it will work with a column spelled " EMail ".
|
143
|
+
|
144
|
+
Now, email could also be spelled "e-mail", or "mail", or even "courriel"
|
145
|
+
(oh, canada). Let's give it a couple of aliases then:
|
146
|
+
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
column :email, as: [/e.?mail/i, "courriel"]
|
150
|
+
```
|
151
|
+
|
152
|
+
Nice, emails should be downcased though, so let's do this.
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
column :email, as: [/e.?mail/i, "courriel"], to: ->(email) { email.downcase }
|
156
|
+
```
|
157
|
+
|
158
|
+
Now, what if the user does not provide the email column? It's not worth
|
159
|
+
running the import, we should just reject the CSV file right away.
|
160
|
+
That's easy:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
class ImportUserCSV
|
164
|
+
include CSVImporter
|
165
|
+
|
166
|
+
model User
|
167
|
+
|
168
|
+
column :email, required: true
|
169
|
+
end
|
170
|
+
|
171
|
+
import = ImportUserCSV.new(content: "name\nbob")
|
172
|
+
import.valid_header? # => false
|
173
|
+
import.report.status # => :invalid_header
|
174
|
+
import.report.message # => "The following columns are required: 'email'"
|
175
|
+
```
|
176
|
+
|
177
|
+
|
178
|
+
### Upsert
|
179
|
+
|
180
|
+
You usually want to prevent duplicates when importing a CSV file. activerecord-import provides ON CONFLICT support for MySQL, SQLite (IGNORE only), and PostgreSQL. See the activerecord-import wiki for detailed syntax.
|
181
|
+
|
182
|
+
NOTE: If you have set up a unique index on a field and not set an appropriate ON CONFLICT resolution, activerecord_csv_import will raise an exception on the first duplicate insert.
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
class ImportUserCSV
|
186
|
+
include CSVImporter
|
187
|
+
|
188
|
+
model User
|
189
|
+
|
190
|
+
column :email, to: ->(email) { email.downcase }
|
191
|
+
column :first_name
|
192
|
+
column :last_name
|
193
|
+
|
194
|
+
on_duplicate_key(
|
195
|
+
on_duplicate_key_update: {
|
196
|
+
conflict_target: [:email], columns: [:first_name, :last_name]
|
197
|
+
}
|
198
|
+
)
|
199
|
+
end
|
200
|
+
```
|
201
|
+
|
202
|
+
You are now done defining your importer, let's run it!
|
203
|
+
|
204
|
+
### Import from a file, path or string
|
205
|
+
|
206
|
+
You can import from a file, path or just the CSV content. Please note
|
207
|
+
that we currently load the entire file in memory. Feel free to
|
208
|
+
contribute if you need to support CSV files with millions of lines! :)
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
import = ImportUserCSV.new(file: my_file)
|
212
|
+
import = ImportUserCSV.new(path: "tmp/new_users.csv")
|
213
|
+
import = ImportUserCSV.new(content: "email,name\nbob@example.com,bob")
|
214
|
+
```
|
215
|
+
|
216
|
+
### Overwrite configuration at runtime
|
217
|
+
|
218
|
+
It is often needed to change the configuration at runtime, that's quite
|
219
|
+
easy:
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
team = Team.find(1)
|
223
|
+
import = ImportUserCSV.new(file: my_file) do
|
224
|
+
model team.users
|
225
|
+
end
|
226
|
+
```
|
227
|
+
|
228
|
+
### `each_batch` callback
|
229
|
+
|
230
|
+
The number of rows to insert in the bulk query can be set by setting `batch_size` (default 500)
|
231
|
+
|
232
|
+
The each batch callback is triggered after each batch is processed and returns the report object for the full process. This is generally useful when you want to display progress.
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
progress_bar = ProgressBar.new
|
236
|
+
|
237
|
+
UserImport.new(file: my_file) do
|
238
|
+
each_batch do |report|
|
239
|
+
progress_bar.increment(report.progress_percentage)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
```
|
243
|
+
Other available methods are:
|
244
|
+
- total_count
|
245
|
+
- completed_count
|
246
|
+
- failed_rows
|
247
|
+
|
248
|
+
### Validate the header
|
249
|
+
|
250
|
+
On a web application, as soon as a CSV file is uploaded, you can check
|
251
|
+
if it has the required columns. This is handy to fail early an provide
|
252
|
+
the user with a meaningful error message right away.
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
import = ImportUserCSV.new(file: params[:csv_file])
|
256
|
+
import.valid_header? # => false
|
257
|
+
import.report.message # => "The following columns are required: "email""
|
258
|
+
```
|
259
|
+
|
260
|
+
### Run the import and provide feedback to the user
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
import = ImportUserCSV.new(file: params[:csv_file])
|
264
|
+
import.run!
|
265
|
+
import.report.message # => "Import completed."
|
266
|
+
```
|
267
|
+
|
268
|
+
You can get your hands dirty and fetch the errored rows and the
|
269
|
+
associated error message:
|
270
|
+
|
271
|
+
```ruby
|
272
|
+
import.report.invalid_rows.map { |row| [row.model.email, row.errors] }
|
273
|
+
# => [ ['INVALID_EMAIL', 'first_name', 'last_name', { 'email' => 'is not an email' }] ]
|
274
|
+
```
|
275
|
+
|
276
|
+
We do our best to map the errors back to the original column name. So
|
277
|
+
with the following definition:
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
column :email, as: /e.?mail/i
|
281
|
+
```
|
282
|
+
|
283
|
+
and csv:
|
284
|
+
|
285
|
+
```csv
|
286
|
+
E-Mail,name
|
287
|
+
INVALID_EMAIL,bob
|
288
|
+
```
|
289
|
+
|
290
|
+
The error returned should be: `{ "E-Mail" => "is invalid" }`
|
291
|
+
|
292
|
+
### Custom quote char
|
293
|
+
|
294
|
+
You can handle exotic quote chars with the `quote_char` option.
|
295
|
+
|
296
|
+
```csv
|
297
|
+
email,name
|
298
|
+
bob@example.com,'bob "elvis" wilson'
|
299
|
+
```
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
import = ImportUserCSV.new(content: csv_content)
|
303
|
+
import.run!
|
304
|
+
import.report.status
|
305
|
+
# => :invalid_csv_file
|
306
|
+
import.report.messages
|
307
|
+
# => CSV::MalformedCSVError: Illegal quoting in line 2.
|
308
|
+
```
|
309
|
+
|
310
|
+
Let's provide a valid quote char:
|
311
|
+
|
312
|
+
```ruby
|
313
|
+
import = ImportUserCSV.new(content: csv_content, quote_char: "'")
|
314
|
+
import.run!
|
315
|
+
# => [ ["bob@example.com", "bob \"elvis\" wilson"] ]
|
316
|
+
```
|
317
|
+
|
318
|
+
### Custom encoding
|
319
|
+
|
320
|
+
You can handle exotic encodings with the `encoding` option.
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
ImportUserCSV.new(content: "メール,氏名".encode('SJIS'), encoding: 'SJIS:UTF-8')
|
324
|
+
```
|
325
|
+
|
326
|
+
## Development
|
327
|
+
|
328
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
|
329
|
+
|
330
|
+
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` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
331
|
+
|
332
|
+
## Contributing
|
333
|
+
|
334
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/activerecord_csv_importer.
|
335
|
+
|
336
|
+
## License
|
337
|
+
|
338
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
339
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'activerecord_csv_importer/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'activerecord_csv_importer'
|
8
|
+
spec.version = ActiveRecordCSVImporter::VERSION
|
9
|
+
spec.authors = ['Zulfiqar Ali']
|
10
|
+
spec.email = ['desheikh@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = 'A modified version of CSV Import using activerecord-import'
|
13
|
+
spec.homepage = 'https://github.com/desheikh/activerecord_csv_importer'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
|
+
f.match(%r{^(test|spec|features)/})
|
18
|
+
end
|
19
|
+
spec.bindir = 'exe'
|
20
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_dependency 'virtus', '~> 1.0'
|
24
|
+
spec.add_dependency 'activerecord-import', '~> 0.16'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'bundler', '~> 1.13'
|
27
|
+
spec.add_development_dependency 'rake', '~> 10.5'
|
28
|
+
|
29
|
+
spec.add_development_dependency 'rspec', '~> 3.5'
|
30
|
+
spec.add_development_dependency 'rubocop', '~> 0.45'
|
31
|
+
|
32
|
+
spec.add_development_dependency 'guard-rspec', '~> 4.7'
|
33
|
+
spec.add_development_dependency 'guard-rubocop', '~> 1.2'
|
34
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'activerecord_csv_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,67 @@
|
|
1
|
+
module ActiveRecordCSVImporter
|
2
|
+
# Define a column. Called from the DSL via `column.
|
3
|
+
#
|
4
|
+
# Examples
|
5
|
+
#
|
6
|
+
# # the csv column "email" will be assigned to the `email` attribute
|
7
|
+
# column :email
|
8
|
+
#
|
9
|
+
# # the csv column matching /email/i will be assigned to the `email` attribute
|
10
|
+
# column :email, as: /email/i
|
11
|
+
#
|
12
|
+
# # the csv column matching "First name" or "Prénom" will be assigned to the
|
13
|
+
# `first_name` attribute
|
14
|
+
# column :first_name, as: [/first ?name/i, /pr(é|e)nom/i]
|
15
|
+
#
|
16
|
+
# # the csv column "first_name" will be assigned to the `f_name` attribute
|
17
|
+
# column :first_name, to: :f_name
|
18
|
+
#
|
19
|
+
# # email will be downcased
|
20
|
+
# column :email, to: ->(email) { email.downcase }
|
21
|
+
#
|
22
|
+
# # transform `confirmed` to `confirmed_at`
|
23
|
+
# column :confirmed, to: ->(confirmed, model) do
|
24
|
+
# model.confirmed_at = confirmed == "true" ? Time.new(2012) : nil
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
class ColumnDefinition
|
28
|
+
include Virtus.model
|
29
|
+
|
30
|
+
attribute :name, Symbol
|
31
|
+
attribute :to # Symbol or Proc
|
32
|
+
attribute :as # Symbol, String, Regexp, Array
|
33
|
+
attribute :required, Boolean
|
34
|
+
|
35
|
+
# The model attribute that this column targets
|
36
|
+
def attribute
|
37
|
+
if to.is_a?(Symbol)
|
38
|
+
to
|
39
|
+
else
|
40
|
+
name
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# rubocop:disable all
|
45
|
+
# Return true if column definition matches the column name passed in.
|
46
|
+
def match?(column_name, search_query = (as || name))
|
47
|
+
return false if column_name.nil?
|
48
|
+
|
49
|
+
downcased_column_name = column_name.downcase
|
50
|
+
underscored_column_name = downcased_column_name.gsub(/\s+/, '_')
|
51
|
+
|
52
|
+
case search_query
|
53
|
+
when Symbol
|
54
|
+
underscored_column_name == search_query.to_s
|
55
|
+
when String
|
56
|
+
downcased_column_name == search_query.downcase
|
57
|
+
when Regexp
|
58
|
+
column_name =~ search_query
|
59
|
+
when Array
|
60
|
+
search_query.any? { |query| match?(column_name, query) }
|
61
|
+
else
|
62
|
+
raise Error, "Invalid `as`. Should be a Symbol, String, Regexp or Array - was #{as.inspect}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
# rubocop:enable all
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ActiveRecordCSVImporter
|
2
|
+
# The configuration of a ActiveRecordCSVImporter
|
3
|
+
class Config
|
4
|
+
include Virtus.model
|
5
|
+
|
6
|
+
attribute :model
|
7
|
+
attribute :column_definitions, Array[ColumnDefinition], default: proc { [] }
|
8
|
+
attribute :on_duplicate_key, Hash, default: []
|
9
|
+
attribute :batch_size, Integer, default: 500
|
10
|
+
attribute :each_batch_blocks, Array[Proc], default: []
|
11
|
+
|
12
|
+
def each_batch(block)
|
13
|
+
each_batch_blocks << block
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module ActiveRecordCSVImporter
|
2
|
+
# Reads, sanitize and parse a CSV file
|
3
|
+
class CSVReader
|
4
|
+
include Virtus.model
|
5
|
+
|
6
|
+
attribute :content, String
|
7
|
+
attribute :file # IO
|
8
|
+
attribute :path, String
|
9
|
+
attribute :quote_char, String, default: '"'
|
10
|
+
attribute :encoding, String, default: 'UTF-8:UTF-8'
|
11
|
+
|
12
|
+
def csv_rows
|
13
|
+
@csv_rows ||= begin
|
14
|
+
sane_content = sanitize_content(read_content)
|
15
|
+
separator = detect_separator(sane_content)
|
16
|
+
cells = CSV.parse(
|
17
|
+
sane_content,
|
18
|
+
col_sep: separator,
|
19
|
+
quote_char: quote_char,
|
20
|
+
skip_blanks: true,
|
21
|
+
encoding: encoding
|
22
|
+
)
|
23
|
+
sanitize_cells(cells)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the header as an Array of Strings
|
28
|
+
def header
|
29
|
+
@header ||= csv_rows.first
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the rows as an Array of Arrays of Strings
|
33
|
+
def rows
|
34
|
+
@rows ||= csv_rows[1..-1]
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def read_content
|
40
|
+
if content
|
41
|
+
content
|
42
|
+
elsif file
|
43
|
+
file.read
|
44
|
+
elsif path
|
45
|
+
File.open(path).read
|
46
|
+
else
|
47
|
+
raise Error, 'Please provide content, file, or path'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def sanitize_content(csv_content)
|
52
|
+
internal_encoding = encoding.split(':').last
|
53
|
+
# Remove invalid byte sequences
|
54
|
+
csv_content
|
55
|
+
.encode(Encoding.find(internal_encoding), invalid: :replace, undef: :replace, replace: '')
|
56
|
+
.gsub(/\r\r?\n?/, "\n") # Replaces windows line separators with "\n"
|
57
|
+
end
|
58
|
+
|
59
|
+
SEPARATORS = [',', ';', "\t"].freeze
|
60
|
+
|
61
|
+
def detect_separator(csv_content)
|
62
|
+
SEPARATORS.sort_by { |separator| csv_content.count(separator) }.last
|
63
|
+
end
|
64
|
+
|
65
|
+
# Remove trailing white spaces and ensure we always return a string
|
66
|
+
def sanitize_cells(rows)
|
67
|
+
rows.map do |cells|
|
68
|
+
cells.map do |cell|
|
69
|
+
cell ? cell.strip : ''
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ActiveRecordCSVImporter
|
2
|
+
# This Dsl extends a class that includes ActiveRecordCSVImporter
|
3
|
+
# It is a thin proxy to the Config object
|
4
|
+
module Dsl
|
5
|
+
def model(model_klass)
|
6
|
+
config.model = model_klass
|
7
|
+
end
|
8
|
+
|
9
|
+
def column(name, options = {})
|
10
|
+
config.column_definitions << options.merge(name: name)
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_duplicate_key(options)
|
14
|
+
config.on_duplicate_key = options
|
15
|
+
end
|
16
|
+
|
17
|
+
def batch_size(size)
|
18
|
+
config.batch_size = size
|
19
|
+
end
|
20
|
+
|
21
|
+
def each_batch(&block)
|
22
|
+
config.each_batch(block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module ActiveRecordCSVImporter
|
2
|
+
# The CSV Header
|
3
|
+
class Header
|
4
|
+
include Virtus.model
|
5
|
+
|
6
|
+
attribute :column_definitions, Array[ColumnDefinition]
|
7
|
+
attribute :column_names, Array[String]
|
8
|
+
|
9
|
+
def columns
|
10
|
+
column_names.map do |column_name|
|
11
|
+
Column.new(
|
12
|
+
name: column_name,
|
13
|
+
definition: find_column_definition(column_name)
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def column_name_for_model_attribute(attribute)
|
19
|
+
column = columns.find { |c|
|
20
|
+
c.definition.attribute == attribute if c.definition
|
21
|
+
}
|
22
|
+
column.name if column
|
23
|
+
end
|
24
|
+
|
25
|
+
def valid?
|
26
|
+
missing_required_columns.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns Array[String]
|
30
|
+
def required_columns
|
31
|
+
column_definitions.select(&:required?).map(&:name)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns Array[String]
|
35
|
+
def missing_required_columns
|
36
|
+
(column_definitions.select(&:required?) - columns.map(&:definition))
|
37
|
+
.map(&:name).map(&:to_s)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns Array[String]
|
41
|
+
def missing_columns
|
42
|
+
(column_definitions - columns.map(&:definition)).map(&:name).map(&:to_s)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def find_column_definition(name)
|
48
|
+
column_definitions.find { |column_definition|
|
49
|
+
column_definition.match?(name)
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def column_definition_names
|
54
|
+
column_definitions.map(&:name).map(&:to_s)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module ActiveRecordCSVImporter
|
2
|
+
# The Report you get back from an import.
|
3
|
+
#
|
4
|
+
# * It has a status (pending, invalid_csv_file, invalid_header, in_progress, done, aborted)
|
5
|
+
# * It lists out missing columns
|
6
|
+
# * It reports parser_error
|
7
|
+
# * It lists out (created / updated) * (success / failed) records
|
8
|
+
# * It provides a human readable message
|
9
|
+
#
|
10
|
+
class Report
|
11
|
+
include Virtus.model
|
12
|
+
|
13
|
+
attribute :status, Symbol, default: proc { :pending }
|
14
|
+
|
15
|
+
attribute :missing_columns, Array[Symbol], default: proc { [] }
|
16
|
+
|
17
|
+
attribute :parser_error, String
|
18
|
+
|
19
|
+
attribute :total_count, Integer, default: 0
|
20
|
+
attribute :completed_count, Integer, default: 0
|
21
|
+
attribute :invalid_rows, Array[Array], default: {}
|
22
|
+
|
23
|
+
attribute :message_generator, Class, default: proc { ReportMessage }
|
24
|
+
|
25
|
+
def progress_percentage
|
26
|
+
return 0 if total_count.zero?
|
27
|
+
(completed_count.to_f / total_count * 100).round
|
28
|
+
end
|
29
|
+
|
30
|
+
def success?
|
31
|
+
done? && invalid_rows.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
def pending?
|
35
|
+
status == :pending
|
36
|
+
end
|
37
|
+
|
38
|
+
def in_progress?
|
39
|
+
status == :in_progress
|
40
|
+
end
|
41
|
+
|
42
|
+
def done?
|
43
|
+
status == :done
|
44
|
+
end
|
45
|
+
|
46
|
+
def aborted?
|
47
|
+
status == :aborted
|
48
|
+
end
|
49
|
+
|
50
|
+
def invalid_header?
|
51
|
+
status == :invalid_header
|
52
|
+
end
|
53
|
+
|
54
|
+
def invalid_csv_file?
|
55
|
+
status == :invalid_csv_file
|
56
|
+
end
|
57
|
+
|
58
|
+
def pending!
|
59
|
+
self.status = :pending
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def in_progress!
|
64
|
+
self.status = :in_progress
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def done!
|
69
|
+
self.status = :done
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
def aborted!
|
74
|
+
self.status = :aborted
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
def invalid_header!
|
79
|
+
self.status = :invalid_header
|
80
|
+
self
|
81
|
+
end
|
82
|
+
|
83
|
+
def invalid_csv_file!
|
84
|
+
self.status = :invalid_csv_file
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
def message
|
89
|
+
message_generator.call(self)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ActiveRecordCSVImporter
|
2
|
+
# Generate a human readable message for the given report.
|
3
|
+
class ReportMessage
|
4
|
+
def self.call(report)
|
5
|
+
new(report).to_s
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(report)
|
9
|
+
@report = report
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :report
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
send("report_#{report.status}")
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def report_pending
|
21
|
+
"Import hasn't started yet"
|
22
|
+
end
|
23
|
+
|
24
|
+
def report_in_progress
|
25
|
+
'Import in progress'
|
26
|
+
end
|
27
|
+
|
28
|
+
def report_done
|
29
|
+
'Import completed'
|
30
|
+
end
|
31
|
+
|
32
|
+
def report_invalid_header
|
33
|
+
"The following columns are required: #{report.missing_columns.join(', ')}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def report_invalid_csv_file
|
37
|
+
report.parser_error
|
38
|
+
end
|
39
|
+
|
40
|
+
def report_aborted
|
41
|
+
'Import aborted'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ActiveRecordCSVImporter
|
2
|
+
# A Row from the CSV file.
|
3
|
+
#
|
4
|
+
# returns a formatted version of the row based of the to proc
|
5
|
+
class Row
|
6
|
+
include Virtus.model
|
7
|
+
|
8
|
+
attribute :header, Header
|
9
|
+
attribute :row_array, Array[String]
|
10
|
+
|
11
|
+
# A hash with this row's attributes
|
12
|
+
def csv_attributes
|
13
|
+
@csv_attributes ||= Hash[header.column_names.zip(row_array)]
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_a
|
17
|
+
header.columns.each_with_object([]) do |column, memo|
|
18
|
+
value = csv_attributes[column.name]
|
19
|
+
begin
|
20
|
+
value = value.dup if value
|
21
|
+
rescue TypeError
|
22
|
+
# can't dup Symbols, Integer etc...
|
23
|
+
end
|
24
|
+
|
25
|
+
next if column.definition.nil?
|
26
|
+
|
27
|
+
memo << get_attribute(column.definition, value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set the attribute using the column_definition and the csv_value
|
32
|
+
def get_attribute(column_definition, csv_value)
|
33
|
+
if column_definition.to && column_definition.to.is_a?(Proc)
|
34
|
+
column_definition.to.call(csv_value)
|
35
|
+
else
|
36
|
+
column_definition.attribute
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module ActiveRecordCSVImporter
|
2
|
+
# Do the actual import.
|
3
|
+
#
|
4
|
+
# It iterates over the rows' models and persist them. It returns a `Report`.
|
5
|
+
class Runner
|
6
|
+
def self.call(*args)
|
7
|
+
new(*args).call
|
8
|
+
end
|
9
|
+
|
10
|
+
include Virtus.model
|
11
|
+
|
12
|
+
attribute :model
|
13
|
+
attribute :header
|
14
|
+
attribute :rows, Array[CSV::Row]
|
15
|
+
attribute :config, Object
|
16
|
+
|
17
|
+
attribute :report, Report, default: proc { Report.new }
|
18
|
+
|
19
|
+
# Persist the csv rows and return a Report
|
20
|
+
def call
|
21
|
+
report.total_count = rows.count
|
22
|
+
report.in_progress!
|
23
|
+
persist_rows!
|
24
|
+
report.done!
|
25
|
+
|
26
|
+
report
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def persist_rows!
|
32
|
+
rows.in_groups_of(config.batch_size, false) do |set|
|
33
|
+
response = import_rows(set)
|
34
|
+
|
35
|
+
add_to_report(response, set)
|
36
|
+
config.each_batch_blocks.each { |block| block.call(report) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def import_rows(set)
|
41
|
+
config.model.import(header.column_names.dup, set, config.on_duplicate_key)
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_to_report(response, set)
|
45
|
+
report.completed_count += set.length
|
46
|
+
|
47
|
+
response.failed_instances.each do |model|
|
48
|
+
report.invalid_rows << (columns_for(model) << build_error(model))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def columns_for(model)
|
53
|
+
model.attributes.values_at(*header.column_names)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Error from the model mapped back to the CSV header if we can
|
57
|
+
def build_error(model)
|
58
|
+
Hash[
|
59
|
+
model.errors.map do |attribute, errors|
|
60
|
+
column_name = header.column_name_for_model_attribute(attribute)
|
61
|
+
column_name ? [column_name, errors] : [attribute, errors]
|
62
|
+
end
|
63
|
+
]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'virtus'
|
3
|
+
|
4
|
+
require 'activerecord_csv_importer/version'
|
5
|
+
require 'activerecord_csv_importer/csv_reader'
|
6
|
+
require 'activerecord_csv_importer/column_definition'
|
7
|
+
require 'activerecord_csv_importer/column'
|
8
|
+
require 'activerecord_csv_importer/header'
|
9
|
+
require 'activerecord_csv_importer/row'
|
10
|
+
require 'activerecord_csv_importer/report'
|
11
|
+
require 'activerecord_csv_importer/report_message'
|
12
|
+
require 'activerecord_csv_importer/runner'
|
13
|
+
require 'activerecord_csv_importer/config'
|
14
|
+
require 'activerecord_csv_importer/dsl'
|
15
|
+
|
16
|
+
module ActiveRecordCSVImporter
|
17
|
+
class Error < StandardError; end
|
18
|
+
|
19
|
+
# Setup DSL and config object
|
20
|
+
def self.included(klass)
|
21
|
+
klass.extend(Dsl)
|
22
|
+
klass.define_singleton_method(:config) do
|
23
|
+
@config ||= Config.new
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Instance level config will run against this configurator
|
28
|
+
class Configurator < Struct.new(:config)
|
29
|
+
include Dsl
|
30
|
+
end
|
31
|
+
|
32
|
+
# Defines the path, file or content of the csv file.
|
33
|
+
# Also allows you to overwrite the configuration at runtime.
|
34
|
+
#
|
35
|
+
# Example:
|
36
|
+
#
|
37
|
+
# .new(file: my_csv_file)
|
38
|
+
# .new(path: "subscribers.csv", model: newsletter.subscribers)
|
39
|
+
#
|
40
|
+
def initialize(*args, &block)
|
41
|
+
@csv = CSVReader.new(*args)
|
42
|
+
@config = self.class.config.dup
|
43
|
+
@config.attributes = args.last
|
44
|
+
@report = Report.new
|
45
|
+
Configurator.new(@config).instance_exec(&block) if block
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :csv, :report, :config
|
49
|
+
|
50
|
+
# Initialize and return the `Header` for the current CSV file
|
51
|
+
def header
|
52
|
+
@header ||= Header.new(
|
53
|
+
column_definitions: config.column_definitions,
|
54
|
+
column_names: csv.header
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Initialize and return the `Row`s for the current CSV file
|
59
|
+
def rows
|
60
|
+
csv.rows.map { |row_array|
|
61
|
+
Row.new(header: header, row_array: row_array).to_a
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def valid_header?
|
66
|
+
if @report.pending?
|
67
|
+
if header.valid?
|
68
|
+
@report = Report.new(status: :pending)
|
69
|
+
else
|
70
|
+
@report = Report.new(
|
71
|
+
status: :invalid_header,
|
72
|
+
missing_columns: header.missing_required_columns
|
73
|
+
)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
header.valid?
|
78
|
+
end
|
79
|
+
|
80
|
+
# Run the import. Return a Report.
|
81
|
+
def run!
|
82
|
+
if valid_header?
|
83
|
+
@report = Runner.call(header: header, rows: rows, config: config)
|
84
|
+
else
|
85
|
+
@report
|
86
|
+
end
|
87
|
+
rescue CSV::MalformedCSVError => e
|
88
|
+
@report = Report.new(status: :invalid_csv_file, parser_error: e.message)
|
89
|
+
end
|
90
|
+
end
|
metadata
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord_csv_importer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Zulfiqar Ali
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-11-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: virtus
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord-import
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.16'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.16'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.13'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.13'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.5'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.5'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.5'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.5'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.45'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.45'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: guard-rspec
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '4.7'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '4.7'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: guard-rubocop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.2'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.2'
|
125
|
+
description:
|
126
|
+
email:
|
127
|
+
- desheikh@gmail.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".gitignore"
|
133
|
+
- ".rspec"
|
134
|
+
- ".rubocop.yml"
|
135
|
+
- ".travis.yml"
|
136
|
+
- Gemfile
|
137
|
+
- Guardfile
|
138
|
+
- LICENSE.txt
|
139
|
+
- README.md
|
140
|
+
- Rakefile
|
141
|
+
- activerecord_csv_importer.gemspec
|
142
|
+
- bin/console
|
143
|
+
- bin/setup
|
144
|
+
- lib/activerecord_csv_importer.rb
|
145
|
+
- lib/activerecord_csv_importer/column.rb
|
146
|
+
- lib/activerecord_csv_importer/column_definition.rb
|
147
|
+
- lib/activerecord_csv_importer/config.rb
|
148
|
+
- lib/activerecord_csv_importer/csv_reader.rb
|
149
|
+
- lib/activerecord_csv_importer/dsl.rb
|
150
|
+
- lib/activerecord_csv_importer/header.rb
|
151
|
+
- lib/activerecord_csv_importer/report.rb
|
152
|
+
- lib/activerecord_csv_importer/report_message.rb
|
153
|
+
- lib/activerecord_csv_importer/row.rb
|
154
|
+
- lib/activerecord_csv_importer/runner.rb
|
155
|
+
- lib/activerecord_csv_importer/version.rb
|
156
|
+
homepage: https://github.com/desheikh/activerecord_csv_importer
|
157
|
+
licenses:
|
158
|
+
- MIT
|
159
|
+
metadata: {}
|
160
|
+
post_install_message:
|
161
|
+
rdoc_options: []
|
162
|
+
require_paths:
|
163
|
+
- lib
|
164
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
165
|
+
requirements:
|
166
|
+
- - ">="
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: '0'
|
169
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
requirements: []
|
175
|
+
rubyforge_project:
|
176
|
+
rubygems_version: 2.5.1
|
177
|
+
signing_key:
|
178
|
+
specification_version: 4
|
179
|
+
summary: A modified version of CSV Import using activerecord-import
|
180
|
+
test_files: []
|