csv-importer 0.1.3 → 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 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