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.
- checksums.yaml +4 -4
- data/README.html +1108 -0
- data/README.md +338 -17
- data/lib/csv_importable/csv_importer.rb +3 -3
- data/lib/csv_importable/importable.rb +0 -3
- data/lib/csv_importable/version.rb +1 -1
- metadata +2 -1
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
|
22
|
-
2. Create a `RowImporter` class: this class handles the logic surrounding how one row in the CSV should be
|
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,
|
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 <
|
41
|
+
class Import < ApplicationRecord
|
40
42
|
include CSVImportable::Importable
|
41
43
|
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
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.
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
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!"
|
145
|
+
print("Imported completed successfully!")
|
146
146
|
when Statuses::ERROR
|
147
|
-
print("\nImported failed, all changes have been rolled back.\n\n"
|
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"
|
149
|
+
print(" #{results[Statuses::ERROR]}\n\n")
|
150
150
|
else
|
151
151
|
results[:results].each { |result| print(" #{result}\n") }
|
152
152
|
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.
|
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
|