csv-importer 0.1.3 → 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/CHANGELOG.md +47 -4
- data/README.md +53 -5
- data/lib/csv_importer.rb +11 -5
- data/lib/csv_importer/config.rb +6 -1
- data/lib/csv_importer/csv_reader.rb +2 -1
- data/lib/csv_importer/dsl.rb +8 -2
- data/lib/csv_importer/row.rb +7 -5
- data/lib/csv_importer/runner.rb +2 -0
- data/lib/csv_importer/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9de355b09e3f3610054a0e2ca5c7f27b6e333c1c
|
4
|
+
data.tar.gz: 759a79fb7217f50959c265cf433351460a21e7f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41d3f8ffff6fa2b92bf02a15c02e9713171d44c898efc929ff8daa79a6f7f9b6a9a666f586abb4874d4a7e738a58d2e2d72035fa447f2c80d5a62bf4ddefafd2
|
7
|
+
data.tar.gz: 721a1e4e3b86de3e633d5c62bb7cd5f602af14e82daa2257af1d9bb88bf3ef07e3ef3201ac327434a1310467210478cce1603600b4cb7c82fa88f4b47f15d721
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,41 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
-
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
## Unreleased
|
6
|
+
|
7
|
+
## [0.2.0] - 2015-07-24
|
8
|
+
|
9
|
+
### Added
|
10
|
+
|
11
|
+
* `after_save` callback.
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
progress_bar = ProgressBar.new
|
15
|
+
|
16
|
+
UserImport.new(file: my_file) do
|
17
|
+
after_save do |user|
|
18
|
+
progress_bar.increment
|
19
|
+
end
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
* You can define a composite identifier made of multiple columns.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
# Update records with matching company_id AND employee_id
|
27
|
+
identifier :company_id, :employee_id
|
28
|
+
```
|
29
|
+
|
30
|
+
* You can set a custom `quote_char` at runtime. ([#26][] by [@shvetsovdm][])
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
UserImport.new(file: csv_file, quote_char: "'")
|
34
|
+
```
|
35
|
+
|
36
|
+
## [0.1.3] - 2015-06-19
|
37
|
+
|
38
|
+
### Added
|
4
39
|
|
5
40
|
* You can now change the configuration at runtime. Example:
|
6
41
|
|
@@ -19,15 +54,23 @@ before saving it.
|
|
19
54
|
enables you to use `id` as an identifier and import new entries
|
20
55
|
without having to provide an `id`
|
21
56
|
|
22
|
-
|
57
|
+
## [0.1.2] - 2015-06-15
|
58
|
+
|
59
|
+
### Fixed
|
23
60
|
|
24
61
|
* `run!` was not *returning* a report object when the header was invalid.
|
25
62
|
|
26
|
-
|
63
|
+
## [0.1.1] - 2015-06-12
|
64
|
+
|
65
|
+
### Changed
|
27
66
|
|
28
67
|
* When calling `run!` on an import with invalid header we update the
|
29
68
|
report object instead of raising an exception.
|
30
69
|
|
31
|
-
|
70
|
+
## [0.1.0] - 2015-06-11
|
32
71
|
|
33
72
|
* Initial Release
|
73
|
+
|
74
|
+
<!--- The following link definition list is generated by PimpMyChangelog --->
|
75
|
+
[#26]: https://github.com/BrewhouseTeam/csv-importer/issues/26
|
76
|
+
[@shvetsovdm]: https://github.com/shvetsovdm
|
data/README.md
CHANGED
@@ -24,8 +24,7 @@ Reporting progress and errors to the end-user is also key for a good
|
|
24
24
|
experience.
|
25
25
|
|
26
26
|
I went through this many times so I decided to build CSV Importer to
|
27
|
-
save us
|
28
|
-
|
27
|
+
save us the trouble.
|
29
28
|
|
30
29
|
CSV Importer provides:
|
31
30
|
|
@@ -33,6 +32,9 @@ CSV Importer provides:
|
|
33
32
|
* good reporting to the end user
|
34
33
|
* support for wild encodings and CSV formats.
|
35
34
|
|
35
|
+
It is compatible with ActiveRecord 4+ and any ORM that implements
|
36
|
+
the class methods `transaction` and `find_by` and the instance method `save`.
|
37
|
+
|
36
38
|
## Usage tldr;
|
37
39
|
|
38
40
|
Define your CSVImporter:
|
@@ -48,7 +50,7 @@ class ImportUserCSV
|
|
48
50
|
column :last_name, as: [ /last.?name/i, "nom" ]
|
49
51
|
column :published, to: ->(published, user) { user.published_at = published ? Time.now : nil }
|
50
52
|
|
51
|
-
identifier :email # will
|
53
|
+
identifier :email # will update_or_create via :email
|
52
54
|
|
53
55
|
when_invalid :skip # or :abort
|
54
56
|
end
|
@@ -215,6 +217,13 @@ end
|
|
215
217
|
|
216
218
|
And yes, we'll look for an existing record using the downcased email. :)
|
217
219
|
|
220
|
+
You can also define a composite identifier:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
# Update records with matching company_id AND employee_id
|
224
|
+
identifier :company_id, :employee_id
|
225
|
+
```
|
226
|
+
|
218
227
|
### Skip or Abort on error
|
219
228
|
|
220
229
|
By default, we skip invalid records and report errors back to the user.
|
@@ -266,13 +275,12 @@ import = ImportUserCSV.new(file: my_file) do
|
|
266
275
|
end
|
267
276
|
```
|
268
277
|
|
269
|
-
###
|
278
|
+
### `after_build` and `after_save` callbacks
|
270
279
|
|
271
280
|
You can preset attributes (or perform any changes to the model) at
|
272
281
|
configuration or runtime using `after_build`
|
273
282
|
|
274
283
|
```ruby
|
275
|
-
|
276
284
|
class ImportUserCSV
|
277
285
|
model User
|
278
286
|
|
@@ -292,6 +300,20 @@ import = ImportUserCSV.new(file: my_file) do
|
|
292
300
|
end
|
293
301
|
```
|
294
302
|
|
303
|
+
The `after_save` callback is run after each call to the method `save` no
|
304
|
+
matter it fails or succeeds. It is quite handy to keep track of
|
305
|
+
progress.
|
306
|
+
|
307
|
+
```ruby
|
308
|
+
progress_bar = ProgressBar.new
|
309
|
+
|
310
|
+
UserImport.new(file: my_file) do
|
311
|
+
after_save do |user|
|
312
|
+
progress_bar.increment
|
313
|
+
end
|
314
|
+
end
|
315
|
+
```
|
316
|
+
|
295
317
|
|
296
318
|
### Validate the header
|
297
319
|
|
@@ -337,6 +359,32 @@ INVALID_EMAIL,bob
|
|
337
359
|
|
338
360
|
The error returned should be: `{ "E-Mail" => "is invalid" }`
|
339
361
|
|
362
|
+
### Custom quote char
|
363
|
+
|
364
|
+
You can handle exotic quote chars with the `quote_char` option.
|
365
|
+
|
366
|
+
```csv
|
367
|
+
email,name
|
368
|
+
bob@example.com,'bob "elvis" wilson'
|
369
|
+
```
|
370
|
+
|
371
|
+
```ruby
|
372
|
+
import = ImportUserCSV.new(content: csv_content)
|
373
|
+
import.run!
|
374
|
+
import.report.status
|
375
|
+
# => :invalid_csv_file
|
376
|
+
import.report.messages
|
377
|
+
# => CSV::MalformedCSVError: Illegal quoting in line 2.
|
378
|
+
```
|
379
|
+
|
380
|
+
Let's provide a valid quote char:
|
381
|
+
|
382
|
+
```ruby
|
383
|
+
import = ImportUserCSV.new(content: csv_content, quote_char: "'")
|
384
|
+
import.run!
|
385
|
+
# => [ ["bob@example.com", "bob \"elvis\" wilson"] ]
|
386
|
+
```
|
387
|
+
|
340
388
|
## Development
|
341
389
|
|
342
390
|
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.
|
data/lib/csv_importer.rb
CHANGED
@@ -30,14 +30,20 @@ require "csv_importer/dsl"
|
|
30
30
|
module CSVImporter
|
31
31
|
class Error < StandardError; end
|
32
32
|
|
33
|
+
# Setup DSL and config object
|
33
34
|
def self.included(klass)
|
34
35
|
klass.extend(Dsl)
|
35
|
-
klass.include(Dsl)
|
36
36
|
klass.define_singleton_method(:config) do
|
37
37
|
@config ||= Config.new
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
+
# Instance level config will run against this configurator
|
42
|
+
class Configurator < Struct.new(:config)
|
43
|
+
include Dsl
|
44
|
+
end
|
45
|
+
|
46
|
+
|
41
47
|
# Defines the path, file or content of the csv file.
|
42
48
|
# Also allows you to overwrite the configuration at runtime.
|
43
49
|
#
|
@@ -51,7 +57,7 @@ module CSVImporter
|
|
51
57
|
@config = self.class.config.dup
|
52
58
|
@config.attributes = args.last
|
53
59
|
@report = Report.new
|
54
|
-
instance_exec(&block) if block
|
60
|
+
Configurator.new(@config).instance_exec(&block) if block
|
55
61
|
end
|
56
62
|
|
57
63
|
attr_reader :csv, :report, :config
|
@@ -65,7 +71,7 @@ module CSVImporter
|
|
65
71
|
def rows
|
66
72
|
csv.rows.map do |row_array|
|
67
73
|
Row.new(header: header, row_array: row_array, model_klass: config.model,
|
68
|
-
|
74
|
+
identifiers: config.identifiers, after_build_blocks: config.after_build_blocks)
|
69
75
|
end
|
70
76
|
end
|
71
77
|
|
@@ -84,7 +90,8 @@ module CSVImporter
|
|
84
90
|
# Run the import. Return a Report.
|
85
91
|
def run!
|
86
92
|
if valid_header?
|
87
|
-
@report = Runner.call(rows: rows, when_invalid: config.when_invalid
|
93
|
+
@report = Runner.call(rows: rows, when_invalid: config.when_invalid,
|
94
|
+
after_save_blocks: config.after_save_blocks)
|
88
95
|
else
|
89
96
|
@report
|
90
97
|
end
|
@@ -92,4 +99,3 @@ module CSVImporter
|
|
92
99
|
@report = Report.new(status: :invalid_csv_file, parser_error: e.message)
|
93
100
|
end
|
94
101
|
end
|
95
|
-
|
data/lib/csv_importer/config.rb
CHANGED
@@ -5,13 +5,18 @@ module CSVImporter
|
|
5
5
|
|
6
6
|
attribute :model
|
7
7
|
attribute :column_definitions, Array[ColumnDefinition], default: proc { [] }
|
8
|
-
attribute :
|
8
|
+
attribute :identifiers, Array[Symbol], default: []
|
9
9
|
attribute :when_invalid, Symbol, default: proc { :skip }
|
10
10
|
attribute :after_build_blocks, Array[Proc], default: []
|
11
|
+
attribute :after_save_blocks, Array[Proc], default: []
|
11
12
|
|
12
13
|
def after_build(block)
|
13
14
|
self.after_build_blocks << block
|
14
15
|
end
|
16
|
+
|
17
|
+
def after_save(block)
|
18
|
+
self.after_save_blocks << block
|
19
|
+
end
|
15
20
|
end
|
16
21
|
end
|
17
22
|
|
@@ -7,12 +7,13 @@ module CSVImporter
|
|
7
7
|
attribute :content, String
|
8
8
|
attribute :file # IO
|
9
9
|
attribute :path, String
|
10
|
+
attribute :quote_char, String, default: '"'
|
10
11
|
|
11
12
|
def csv_rows
|
12
13
|
@csv_rows ||= begin
|
13
14
|
sane_content = sanitize_content(read_content)
|
14
15
|
separator = detect_separator(sane_content)
|
15
|
-
cells = CSV.parse(sane_content, col_sep: separator)
|
16
|
+
cells = CSV.parse(sane_content, col_sep: separator, quote_char: quote_char)
|
16
17
|
sanitize_cells(cells)
|
17
18
|
end
|
18
19
|
end
|
data/lib/csv_importer/dsl.rb
CHANGED
@@ -10,10 +10,12 @@ module CSVImporter
|
|
10
10
|
config.column_definitions << options.merge(name: name)
|
11
11
|
end
|
12
12
|
|
13
|
-
def identifier(
|
14
|
-
config.
|
13
|
+
def identifier(*identifiers)
|
14
|
+
config.identifiers = identifiers
|
15
15
|
end
|
16
16
|
|
17
|
+
alias_method :identifiers, :identifier
|
18
|
+
|
17
19
|
def when_invalid(action)
|
18
20
|
config.when_invalid = action
|
19
21
|
end
|
@@ -21,5 +23,9 @@ module CSVImporter
|
|
21
23
|
def after_build(&block)
|
22
24
|
config.after_build(block)
|
23
25
|
end
|
26
|
+
|
27
|
+
def after_save(&block)
|
28
|
+
config.after_save(block)
|
29
|
+
end
|
24
30
|
end
|
25
31
|
end
|
data/lib/csv_importer/row.rb
CHANGED
@@ -9,13 +9,14 @@ module CSVImporter
|
|
9
9
|
attribute :header, Header
|
10
10
|
attribute :row_array, Array[String]
|
11
11
|
attribute :model_klass
|
12
|
-
attribute :
|
12
|
+
attribute :identifiers, Array[Symbol]
|
13
13
|
attribute :after_build_blocks, Array[Proc], default: []
|
14
14
|
|
15
15
|
# The model to be persisted
|
16
16
|
def model
|
17
17
|
@model ||= begin
|
18
18
|
model = find_or_build_model
|
19
|
+
|
19
20
|
set_attributes(model)
|
20
21
|
end
|
21
22
|
end
|
@@ -85,13 +86,14 @@ module CSVImporter
|
|
85
86
|
end
|
86
87
|
|
87
88
|
def find_model
|
88
|
-
return nil if
|
89
|
+
return nil if identifiers.empty?
|
89
90
|
|
90
91
|
model = build_model
|
91
92
|
set_attributes(model)
|
92
|
-
|
93
|
-
|
94
|
-
|
93
|
+
query = Hash[
|
94
|
+
identifiers.map { |identifier| [ identifier, model.public_send(identifier) ] }
|
95
|
+
]
|
96
|
+
model_klass.find_by(query)
|
95
97
|
end
|
96
98
|
|
97
99
|
def build_model
|
data/lib/csv_importer/runner.rb
CHANGED
@@ -11,6 +11,7 @@ module CSVImporter
|
|
11
11
|
|
12
12
|
attribute :rows, Array[Row]
|
13
13
|
attribute :when_invalid, Symbol
|
14
|
+
attribute :after_save_blocks, Array[Proc], default: []
|
14
15
|
|
15
16
|
attribute :report, Report, default: proc { Report.new }
|
16
17
|
|
@@ -58,6 +59,7 @@ module CSVImporter
|
|
58
59
|
end
|
59
60
|
|
60
61
|
add_to_report(row, tags)
|
62
|
+
after_save_blocks.each { |block| block.call(row.model) }
|
61
63
|
end
|
62
64
|
end
|
63
65
|
end
|
data/lib/csv_importer/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: csv-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
|
- Philippe Creux
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-07-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: virtus
|