csv_importable 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -18,17 +18,19 @@ And then execute:
18
18
 
19
19
  High level steps:
20
20
 
21
- 1. Create an `Import` model: this model handles and stores the file, status, and results of the import for the user to see.
22
- 2. Create a `RowImporter` class: this class handles the logic surrounding how one row in the CSV should be added to the database.
21
+ 1. Create an `Import` model: this model stores the file, status, and results of the import for the user to see.
22
+ 2. Create a `RowImporter` class: this class handles the logic surrounding how one row in the CSV should be imported.
23
+ 3. Create route(s), controller, and view(s) to allow your users to upload a CSV and return comprehensive, easy to understand error messages for your users to correct the CSV and re-upload.
23
24
 
24
- Please note, it is also possible to implement an `Importer` class, which handles the logic surrounding how the entire file is imported. This is not usually needed though.
25
+ Please note, it is also possible to implement an `Importer` class, which handles the logic surrounding how the entire file is imported. This is not usually needed though as our default `Importer` will take care of the heavy lifting.
25
26
 
27
+ If any errors happen during the upload process, they are recorded, stored, and ultimately displayed to the user for an opportunity to correct the CSV file.
26
28
 
27
29
  ### Create Import Model
28
30
 
29
31
  This model handles and stores the file, status, and results of the import for the user to see. By storing the file and results, we can process the import in the background when the file is too large to process real-time, and then email the user when the import is finished.
30
32
 
31
- Note: if you're not using Paperclip, you can modify `file` to be a string or some other data type that helps you find the file for the `read_file` method, which is really the only required field as it relates to the uploaded file.
33
+ Note: if you're not using Paperclip, don't worry about implementing the `file` field. The important thing is that you implement a `read_file` method on your `Import` class so we know how to get the StringIO data for your CSV file.
32
34
 
33
35
  $ rails g model Import status:string results:text type:string file:attachment should_replace:boolean
34
36
  $ bundle exec rake db:migrate
@@ -36,12 +38,18 @@ Note: if you're not using Paperclip, you can modify `file` to be a string or som
36
38
  Change the Import class to look something like below:
37
39
 
38
40
  ```ruby
39
- class Import < ActiveRecord::Base
41
+ class Import < ApplicationRecord
40
42
  include CSVImportable::Importable
41
43
 
42
- def row_importer_class
43
- # e.g. UserRowImporter (see next section)
44
- end
44
+ has_attached_file :file
45
+ validates_attachment :file, content_type: { content_type: ['text/csv']}, message: "is not in CSV format"
46
+
47
+ ## for background processing
48
+ ## note - this code is for Delayed Jobs,
49
+ ## you may need to implement something different
50
+ ## for a different background job processor
51
+ # handle_asynchronously :process_in_background
52
+
45
53
 
46
54
  def read_file
47
55
  # needs to return StringIO of file
@@ -66,24 +74,115 @@ class Import < ActiveRecord::Base
66
74
  end
67
75
  ```
68
76
 
77
+ And then create a subclass that should correspond to the specific importing task you are implementing. For example, if you are trying to import users from a CSV, you might implement a `UserImport` class which inherits from `Import`:
78
+
79
+ app/models/user_import.rb:
80
+
81
+ ```ruby
82
+ class UserImport < Import
83
+ def row_importer_class
84
+ UserRowImporter
85
+ end
86
+ end
87
+ ```
88
+
89
+ The only method that you need to define here is the `row_importer_class`, which tells `csv_importable` how to import each row in the CSV. Let's take a look.
90
+
69
91
  ### Create RowImporter Class
70
92
 
71
- this class handles the logic surrounding how one row in the CSV should be added to the database. You need only (1) inherit from `CSVImportable::CSVImporter` and (2) implement the `import_row` method.
93
+ The `RowImporter` class handles the logic surrounding how one row in the CSV should be imported and added to the database. You need only (1) inherit from `CSVImportable::CSVImporter` and (2) implement the `import_row` method.
72
94
 
73
95
  ```ruby
74
96
  class UserRowImporter < CSVImportable::CSVImporter
75
97
  def import_row
76
- user = User.new
77
- user.email = pull_string('email', required: true)
78
- user.first_name = pull_string('first_name', required: true)
79
- user.last_name = pull_string('last_name', required: true)
80
- user.birthdate = pull_date('birthdate') # format: YYYYMMDD
81
- user.salary = pull_float('salary')
98
+ user = User.create(
99
+ email: pull_string('email', required: true),
100
+ first_name: pull_string('first_name', required: true),
101
+ last_name: pull_string('last_name', required: true),
102
+ birthdate: pull_date('birthdate'), # format: YYYYMMDD
103
+ salary: pull_float('salary')
104
+ )
82
105
  end
83
106
  end
84
107
  ```
85
108
 
86
- #### Parsers
109
+ See that `pull_string` method? See the Parsers section below for more information on how to take advantage of them.
110
+
111
+ ### Creating an Import UI for your users
112
+
113
+ Let's say you want to create a UI for your users to upload a CSV of users for your new `UserImport`.
114
+
115
+ Routes:
116
+
117
+ ```ruby
118
+ resources :user_imports, only: [:new, :create, :index]
119
+ ```
120
+
121
+ Controller (app/controllers/user_imports_controller.rb):
122
+
123
+ ```ruby
124
+ class UserImportsController < ApplicationController
125
+ def new
126
+ @import = UserImport.new
127
+ end
128
+
129
+ def create
130
+ @import = UserImport.new(params[:user_import])
131
+
132
+ if @import.import!
133
+ redirect_to :back, notice: "The file is being imported."
134
+ else
135
+ render :new
136
+ end
137
+ end
138
+
139
+ def index
140
+ @imports = UserImport.all
141
+ end
142
+ end
143
+ ```
144
+
145
+ New view (app/views/user_imports/new.html.erb):
146
+
147
+ ```erb
148
+ <%= form_for @import, html: { multipart: true } do |f| %>
149
+ <% if f.object.failed? %>
150
+ <ul>
151
+ <% f.object.formatted_errors.each do |error| %>
152
+ <li><%= error %></li>
153
+ <% end %>
154
+ </ul>
155
+ <% end %>
156
+
157
+ <%= f.file_field :file %>
158
+ <%= f.submit %>
159
+ <% end %>
160
+ ```
161
+
162
+ Index view (app/views/user_imports/index.html.erb):
163
+
164
+ ```erb
165
+ <ul>
166
+ <% @imports.each do |import| %>
167
+ <li>
168
+ <p>Status: <%= import.display_status %></p>
169
+ <p>Number of Records: <%= import.number_of_records_imported %></p>
170
+ <% if import.failed? %>
171
+ <p>Errors:</p>
172
+ <ul>
173
+ <% import.formatted_errors.each do |error| %>
174
+ <li><%= error %></li>
175
+ <% end %>
176
+ </ul>
177
+ <% end %>
178
+ </li>
179
+ <% end %>
180
+ </ul>
181
+ ```
182
+
183
+ ## Advanced Usage
184
+
185
+ ### Parsers
87
186
 
88
187
  To assist you in getting data out of your CSV, we've implemented some basic parsers. These parsers will grab the raw data for the particular row/column and attempt to coerce it into the correct type (e.g. take string from CSV and convert to float).
89
188
 
@@ -94,10 +193,232 @@ If the parser fails to coerce the data properly, it will add an error message to
94
193
  - pull_date
95
194
  - pull_float
96
195
  - pull_integer
97
- - pull_select
196
+ - pull_select (e.g. `pull_select('my_boolean_column', options: ['Yes', 'No'])`)
98
197
 
99
198
  Basic syntax: `pull_string(column_key, args)` where `column_key` is the CSV header string for the column and `args` is a hash with the following defaults: `{ required: false }`
100
199
 
200
+
201
+ #### Custom Parsers
202
+
203
+ You can build a custom parser by creating a class that inherits from `CSVImportable::TypeParser`. This class needs to implement at least two methods:
204
+
205
+ - `parse_val`
206
+ - `error_message`
207
+
208
+ For example:
209
+
210
+ ```ruby
211
+ class TypeParser::CustomDateTypeParser < CSVImportable::TypeParser
212
+ def parse_val
213
+ Date.strptime(value, '%m-%d-%Y')
214
+ end
215
+
216
+ def error_message
217
+ "Invalid date for column: #{key}"
218
+ end
219
+ end
220
+ ```
221
+
222
+ Now, in your `RowImporter` class you can call: `TypeParser::CustomDateTypeParser.new('my_date_field')` to return a date object when the data is in the right format. If the parser fails to parse the field, it will add the correct error message for your user to review and resolve.
223
+
224
+ #### Ignoring Parsers
225
+
226
+ Inside a `RowImporter` class, you have access to `row` and `headers` methods. For example, you can call `row.field('field_name')` to pull data directly from the CSV.
227
+
228
+ ### ActiveAdmin
229
+
230
+ If you are using ActiveAdmin, consider adding the code below for your admin/import.rb file:
231
+
232
+ ```ruby
233
+ ActiveAdmin.register Import do
234
+ config.filters = false
235
+
236
+ index do
237
+ column 'Status' do |import|
238
+ link_to import.display_status, admin_import_path(import)
239
+ end
240
+ column 'File Uploaded' do |import|
241
+ link_to import.file_file_name, import.file.url
242
+ end
243
+ column 'Records' do |import|
244
+ import.number_of_records_imported
245
+ end
246
+ actions
247
+ end
248
+
249
+ show do
250
+ attributes_table do
251
+ row 'Status' do |import|
252
+ import.display_status
253
+ end
254
+ row 'Errors' do |import|
255
+ if import.failed?
256
+ ul do
257
+ import.formatted_errors.each do |error|
258
+ li style: 'color:red;' do
259
+ error
260
+ end
261
+ end
262
+ end
263
+ else
264
+ 'None'
265
+ end
266
+ end
267
+ row 'File Uploaded' do |import|
268
+ link_to import.file_file_name, import.file.url
269
+ end
270
+ row 'Records Imported' do |import|
271
+ import.number_of_records_imported
272
+ end
273
+ end
274
+ end
275
+
276
+ form :html => { :enctype => "multipart/form-data" } do |f|
277
+ if f.object.failed?
278
+ panel "Errors" do
279
+ ul do
280
+ f.object.formatted_errors.each do |error|
281
+ li style: 'color:red;' do
282
+ error
283
+ end
284
+ end
285
+ end
286
+ end
287
+ end
288
+
289
+ f.inputs "Details" do
290
+ f.input :file, :as => :file, :hint => f.object.file_file_name
291
+ end
292
+ f.actions
293
+ end
294
+
295
+ controller do
296
+ def process_success
297
+ notice = @import.processing? ? 'Your import is being processed' : 'Your import is complete'
298
+ redirect_to admin_import_path(@import), notice: notice
299
+ end
300
+
301
+ def create
302
+ @import = Import.new(params[:import])
303
+
304
+ if @import.import!
305
+ process_success
306
+ else
307
+ return render :new
308
+ end
309
+ end
310
+
311
+ def update
312
+ @import = Import.find(params[:id])
313
+ @import.attributes = params[:import] || {}
314
+
315
+ if @import.import!
316
+ process_success
317
+ else
318
+ return render :edit
319
+ end
320
+ end
321
+ end
322
+ end
323
+ ```
324
+
325
+
326
+ ### One UI to rule them all
327
+
328
+ For the ambitous out there that are trying to build one common UI for your users to implement many different imports, see here.
329
+
330
+ Routes:
331
+
332
+ ```ruby
333
+ resources :user_imports, controller: :imports, type: 'UserImport'
334
+ resources :companies_imports, controller: :imports, type: 'CompanyImport'
335
+ ```
336
+
337
+ Controller:
338
+
339
+ ```ruby
340
+ class ImportsController < ApplicationController
341
+ def index
342
+ @imports = model.all
343
+ end
344
+
345
+ def new
346
+ @import = model.new
347
+ end
348
+
349
+ def create
350
+ @import = model.new(args)
351
+
352
+ if @import.import!
353
+ redirect_to :back, notice: "The file is being imported."
354
+ else
355
+ redirect_to :back, alert: 'There was a problem with the import. Please contact the administrator if the probelm persists.'
356
+ end
357
+ end
358
+
359
+ def edit
360
+ @import = model.find(params[:id])
361
+ end
362
+
363
+ def show
364
+ @import = model.find(params[:id])
365
+ end
366
+
367
+ private
368
+
369
+ def type
370
+ params.fetch(:type, 'Import')
371
+ end
372
+
373
+ def model
374
+ # see below for ensuring this import type is present
375
+ # and then uncomment the following line:
376
+ # raise 'Not a valid import type' unless valid_type?
377
+ type.constantize
378
+ end
379
+
380
+ # need to implement an array of available types
381
+ # in Import class for this to work:
382
+ # def valid_type?
383
+ # Import::Types::ALL.include?(type)
384
+ # end
385
+
386
+ # def redirect_invalid_type
387
+ # flash.now[:alert] = 'Not a valid import type'
388
+ # return redirect_to :back
389
+ # end
390
+ end
391
+ ```
392
+
393
+ app/helpers/imports_helper:
394
+
395
+ ```ruby
396
+ module ImportsHelper
397
+ def url_for_import(import_obj)
398
+ if import_obj.new_record?
399
+ name = import_obj.class.name.underscore.pluralize
400
+ send("#{name}_path")
401
+ else
402
+ name = import_obj.class.name.underscore.pluralize
403
+ send("#{name}_path", import_obj)
404
+ end
405
+ end
406
+ end
407
+ ```
408
+
409
+ Your view may look something like:
410
+
411
+ ```erb
412
+ <%= form_for @import, url: url_for_import(@import), html: { multipart: true } do |f| %>
413
+ <%= f.file_field :file %>
414
+ <%= f.submit %>
415
+ <% end %>
416
+ ```
417
+
418
+ ### Overwriting the `Importer` class
419
+
420
+ TODO: add explanation about how to do this.
421
+
101
422
  ## Contributing
102
423
 
103
424
  Bug reports and pull requests are welcome on GitHub at https://github.com/launchpadlab/csv_importable
@@ -142,11 +142,11 @@ module CSVImportable
142
142
  def print_results(results)
143
143
  case results[:status]
144
144
  when Statuses::SUCCESS
145
- print("Imported completed successfully!".green)
145
+ print("Imported completed successfully!")
146
146
  when Statuses::ERROR
147
- print("\nImported failed, all changes have been rolled back.\n\n".red)
147
+ print("\nImported failed, all changes have been rolled back.\n\n")
148
148
  if results[Statuses::ERROR]
149
- print(" #{results[Statuses::ERROR]}\n\n".red)
149
+ print(" #{results[Statuses::ERROR]}\n\n")
150
150
  else
151
151
  results[:results].each { |result| print(" #{result}\n") }
152
152
  end
@@ -11,9 +11,6 @@ module CSVImportable
11
11
 
12
12
  included do
13
13
  serialize :results, Hash
14
-
15
- has_attached_file :file
16
- validates_attachment :file, content_type: { content_type: ['text/csv']} , message: "is not in CSV format"
17
14
  end
18
15
 
19
16
  DEFAULT_BIG_FILE_THRESHOLD = 10
@@ -1,3 +1,3 @@
1
1
  module CsvImportable
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: csv_importable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Francis
@@ -78,6 +78,7 @@ files:
78
78
  - ".rspec"
79
79
  - ".travis.yml"
80
80
  - Gemfile
81
+ - README.html
81
82
  - README.md
82
83
  - Rakefile
83
84
  - bin/console