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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7ef35d5c77f6bc1eba14f38b6d68737e393ebb69
4
- data.tar.gz: c670a2d4fa3b6dda957170eaeb1e46931ba57521
3
+ metadata.gz: 9de355b09e3f3610054a0e2ca5c7f27b6e333c1c
4
+ data.tar.gz: 759a79fb7217f50959c265cf433351460a21e7f4
5
5
  SHA512:
6
- metadata.gz: 6efcbaf329d95b552f8a43201d816c2f0649ef20d511084e00f6a211f94eaa8f6042f9e04de7892ca3f2892d95f9a1dfa1e819558d56a9d7f0e3f188344eb774
7
- data.tar.gz: d0d251d39386bef710e3773fed1a0d32a3ee3d3a11db5485682748a6ebf128ab045f2d6522d4143982a65473347f36ed67c3a79f0ca8934b500b21b5cf78cbc6
6
+ metadata.gz: 41d3f8ffff6fa2b92bf02a15c02e9713171d44c898efc929ff8daa79a6f7f9b6a9a666f586abb4874d4a7e738a58d2e2d72035fa447f2c80d5a62bf4ddefafd2
7
+ data.tar.gz: 721a1e4e3b86de3e633d5c62bb7cd5f602af14e82daa2257af1d9bb88bf3ef07e3ef3201ac327434a1310467210478cce1603600b4cb7c82fa88f4b47f15d721
@@ -1,6 +1,41 @@
1
1
  # Change Log
2
2
 
3
- ### v0.1.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
- ### v0.1.2
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
- ### v0.1.1
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
- ### v0.1.0
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 a lot of trouble.
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 find_or_update via :email
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
- ### Preset attributes
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.
@@ -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
- identifier: config.identifier, after_build_blocks: config.after_build_blocks)
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
-
@@ -5,13 +5,18 @@ module CSVImporter
5
5
 
6
6
  attribute :model
7
7
  attribute :column_definitions, Array[ColumnDefinition], default: proc { [] }
8
- attribute :identifier, Symbol
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
@@ -10,10 +10,12 @@ module CSVImporter
10
10
  config.column_definitions << options.merge(name: name)
11
11
  end
12
12
 
13
- def identifier(identifier)
14
- config.identifier = identifier
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
@@ -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 :identifier, Symbol
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 identifier.nil?
89
+ return nil if identifiers.empty?
89
90
 
90
91
  model = build_model
91
92
  set_attributes(model)
92
- if value = model.public_send(identifier)
93
- model_klass.public_send("find_by_#{identifier}", value)
94
- end
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module CSVImporter
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.0"
3
3
  end
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.1.3
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-06-20 00:00:00.000000000 Z
11
+ date: 2015-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: virtus