csv_importable 0.1.4 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +119 -23
- data/lib/csv_importable/type_parser/select_type_parser.rb +4 -1
- data/lib/csv_importable/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: d03b574a8314c2eb5c192a7dcb2437cd93a6bf8b
|
4
|
+
data.tar.gz: c24aa7a22db05556ebe510e137d52cbd981ba4dc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 59e81c752e193230d82568b113a084dfe97452a4d5d326c71cd6ed85f2afcb3cc1eb3d1be459690c8ba0a13269babe430d62ee2322b5d8cb36a2f315ac0a6e6e
|
7
|
+
data.tar.gz: 28a2c42b3545a1613adb6e41efade75bc9ea0267cdea9ca5f0154617b61800035cac9f2455a604fe4ecab1b8bfd695b9b2b0670a5d8ec594504804d10983b48c
|
data/README.md
CHANGED
@@ -1,6 +1,26 @@
|
|
1
1
|
# CSV Importable
|
2
2
|
|
3
|
-
|
3
|
+
While it may seem simple on the surface, allowing a user to upload a CSV for inserting or updating multiple records at a time can actually be quite difficult. Here are a few of the tasks involved:
|
4
|
+
|
5
|
+
- For big files, put the import on a background job
|
6
|
+
- store CSV for background processing
|
7
|
+
- send email when complete
|
8
|
+
- store status to keep the user informed on progress
|
9
|
+
- store errors to show the user what went wrong if the import fails
|
10
|
+
- For each row in the CSV, do the following:
|
11
|
+
- Parse the data (for example, extracting a date field)
|
12
|
+
- Find or create objects and their relationships
|
13
|
+
- Record any errors that occur to show the user
|
14
|
+
- If any errors occur during the process, rollback all transactions, show the user the errors, and allow the user to try again
|
15
|
+
|
16
|
+
While this process is certainly complicated, it is consistent enough to justify the existence of a gem.
|
17
|
+
|
18
|
+
The goal of the CSV Importable gem is to allow you to focus on what is unique about your import process: how the data from the CSV should impact your database.
|
19
|
+
|
20
|
+
## Example Rails App
|
21
|
+
|
22
|
+
- Code: https://github.com/LaunchPadLab/example_csv_import
|
23
|
+
- Demo: https://example-csv-import.herokuapp.com/
|
4
24
|
|
5
25
|
## Installation
|
6
26
|
|
@@ -44,6 +64,8 @@ class Import < ApplicationRecord
|
|
44
64
|
has_attached_file :file
|
45
65
|
validates_attachment :file, content_type: { content_type: ['text/csv']}, message: "is not in CSV format"
|
46
66
|
|
67
|
+
validates :file, presence: true
|
68
|
+
|
47
69
|
## for background processing
|
48
70
|
## note - this code is for Delayed Jobs,
|
49
71
|
## you may need to implement something different
|
@@ -54,7 +76,7 @@ class Import < ApplicationRecord
|
|
54
76
|
def read_file
|
55
77
|
# needs to return StringIO of file
|
56
78
|
# for paperclip, use:
|
57
|
-
|
79
|
+
Paperclip.io_adapters.for(file).read
|
58
80
|
end
|
59
81
|
|
60
82
|
def after_async_complete
|
@@ -90,10 +112,10 @@ The only method that you need to define here is the `row_importer_class`, which
|
|
90
112
|
|
91
113
|
### Create RowImporter Class
|
92
114
|
|
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::
|
115
|
+
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::RowImporter` and (2) implement the `import_row` method.
|
94
116
|
|
95
117
|
```ruby
|
96
|
-
class UserRowImporter < CSVImportable::
|
118
|
+
class UserRowImporter < CSVImportable::RowImporter
|
97
119
|
def import_row
|
98
120
|
user = User.create(
|
99
121
|
email: pull_string('email', required: true),
|
@@ -106,19 +128,19 @@ class UserRowImporter < CSVImportable::CSVImporter
|
|
106
128
|
end
|
107
129
|
```
|
108
130
|
|
109
|
-
See that `pull_string` method?
|
131
|
+
See that `pull_string` method? Check out the Parsers section below for more information on how to take advantage of default and custom parsers.
|
110
132
|
|
111
133
|
### Creating an Import UI for your users
|
112
134
|
|
113
|
-
Let's say you want to create a UI for your users to upload a CSV
|
135
|
+
Let's say you want to create a UI for your users to upload a CSV for your new `UserImport`.
|
114
136
|
|
115
|
-
Routes
|
137
|
+
**Routes:**
|
116
138
|
|
117
139
|
```ruby
|
118
|
-
resources :user_imports
|
140
|
+
resources :user_imports
|
119
141
|
```
|
120
142
|
|
121
|
-
Controller (app/controllers/user_imports_controller.rb)
|
143
|
+
**Controller (app/controllers/user_imports_controller.rb):**
|
122
144
|
|
123
145
|
```ruby
|
124
146
|
class UserImportsController < ApplicationController
|
@@ -127,22 +149,53 @@ class UserImportsController < ApplicationController
|
|
127
149
|
end
|
128
150
|
|
129
151
|
def create
|
130
|
-
@import = UserImport.new(
|
152
|
+
@import = UserImport.new(user_import_params)
|
153
|
+
process_import
|
154
|
+
end
|
131
155
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
156
|
+
def edit
|
157
|
+
@import = UserImport.find(params[:id])
|
158
|
+
end
|
159
|
+
|
160
|
+
def update
|
161
|
+
@import = UserImport.find(params[:id])
|
162
|
+
@import.attributes = user_import_params
|
163
|
+
process_import
|
137
164
|
end
|
138
165
|
|
139
166
|
def index
|
140
167
|
@imports = UserImport.all
|
141
168
|
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def process_import
|
173
|
+
if @import.import!
|
174
|
+
return redirect_to user_imports_path, notice: "The file is being imported."
|
175
|
+
else
|
176
|
+
return redirect_to edit_user_import_path(@import)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def user_import_params
|
181
|
+
params.require(:user_import).permit(:file)
|
182
|
+
end
|
142
183
|
end
|
143
184
|
```
|
144
185
|
|
145
|
-
New view (app/views/user_imports/new.html.erb)
|
186
|
+
**New view (app/views/user_imports/new.html.erb):**
|
187
|
+
|
188
|
+
```erb
|
189
|
+
<%= render 'form' %>
|
190
|
+
```
|
191
|
+
|
192
|
+
**Edit view (app/views/user_imports/edit.html.erb):**
|
193
|
+
|
194
|
+
```erb
|
195
|
+
<%= render 'form' %>
|
196
|
+
```
|
197
|
+
|
198
|
+
**Form partial (app/views/user_imports/_form.html.erb):**
|
146
199
|
|
147
200
|
```erb
|
148
201
|
<%= form_for @import, html: { multipart: true } do |f| %>
|
@@ -159,7 +212,7 @@ New view (app/views/user_imports/new.html.erb):
|
|
159
212
|
<% end %>
|
160
213
|
```
|
161
214
|
|
162
|
-
Index view (app/views/user_imports/index.html.erb)
|
215
|
+
**Index view (app/views/user_imports/index.html.erb):**
|
163
216
|
|
164
217
|
```erb
|
165
218
|
<ul>
|
@@ -180,6 +233,42 @@ Index view (app/views/user_imports/index.html.erb):
|
|
180
233
|
</ul>
|
181
234
|
```
|
182
235
|
|
236
|
+
## Send an email once background job finishes
|
237
|
+
|
238
|
+
If the user uploads a large file that exceeds your `big_file_threshold`, you can send an email to the user when it is complete.
|
239
|
+
|
240
|
+
**app/models/import.rb**
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
def async_complete
|
244
|
+
SiteMailer.import_complete(self).deliver_later
|
245
|
+
end
|
246
|
+
```
|
247
|
+
|
248
|
+
**app/mailers/site_mailer.rb**
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
def import_complete(import)
|
252
|
+
@import = import
|
253
|
+
email = 'ryan@example.com' # this could be import.user.email for example
|
254
|
+
mail(to: email, subject: 'Your Import is Complete')
|
255
|
+
end
|
256
|
+
```
|
257
|
+
|
258
|
+
**app/views/site_mailer/import_complete.html.erb**
|
259
|
+
|
260
|
+
```erb
|
261
|
+
<div>
|
262
|
+
<p>Your import finished processing.</p>
|
263
|
+
<p>Status: <span class="<%= @import.status %>"><%= @import.display_status %><span></p>
|
264
|
+
<% if @import.failed? %>
|
265
|
+
<p>Please review your errors here: <%= link_to 'See Errors', import_url(@import.id) %></p>
|
266
|
+
<% else %>
|
267
|
+
<p>You can review your import here: <%= link_to 'Review Import', import_url(@import.id) %></p>
|
268
|
+
<% end %>
|
269
|
+
</div>
|
270
|
+
```
|
271
|
+
|
183
272
|
## Advanced Usage
|
184
273
|
|
185
274
|
### Parsers
|
@@ -193,9 +282,9 @@ If the parser fails to coerce the data properly, it will add an error message to
|
|
193
282
|
- pull_date
|
194
283
|
- pull_float
|
195
284
|
- pull_integer
|
196
|
-
- pull_select (e.g. `pull_select('
|
285
|
+
- pull_select (e.g. `pull_select('color', options: ['Red', 'Green', 'Blue'])`)
|
197
286
|
|
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 }`
|
287
|
+
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, row: row }`
|
199
288
|
|
200
289
|
|
201
290
|
#### Custom Parsers
|
@@ -208,7 +297,7 @@ You can build a custom parser by creating a class that inherits from `CSVImporta
|
|
208
297
|
For example:
|
209
298
|
|
210
299
|
```ruby
|
211
|
-
class
|
300
|
+
class CustomDateTypeParser < CSVImportable::TypeParser
|
212
301
|
def parse_val
|
213
302
|
Date.strptime(value, '%m-%d-%Y')
|
214
303
|
end
|
@@ -219,7 +308,7 @@ class TypeParser::CustomDateTypeParser < CSVImportable::TypeParser
|
|
219
308
|
end
|
220
309
|
```
|
221
310
|
|
222
|
-
Now, in your `RowImporter` class you can call: `
|
311
|
+
Now, in your `RowImporter` class you can call: `CustomDateTypeParser.new('my_date_field', row: row)` 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
312
|
|
224
313
|
#### Ignoring Parsers
|
225
314
|
|
@@ -286,7 +375,12 @@ ActiveAdmin.register Import do
|
|
286
375
|
end
|
287
376
|
end
|
288
377
|
|
289
|
-
|
378
|
+
panel "1. Download Template CSV" do
|
379
|
+
# link to template file that should be in public/
|
380
|
+
link_to 'Download Template CSV', '/example.csv'
|
381
|
+
end
|
382
|
+
|
383
|
+
f.inputs "2. Import CSV" do
|
290
384
|
f.input :file, :as => :file, :hint => f.object.file_file_name
|
291
385
|
end
|
292
386
|
f.actions
|
@@ -300,17 +394,19 @@ ActiveAdmin.register Import do
|
|
300
394
|
|
301
395
|
def create
|
302
396
|
@import = Import.new(params[:import])
|
397
|
+
return render :new unless @import.save
|
303
398
|
|
304
399
|
if @import.import!
|
305
400
|
process_success
|
306
401
|
else
|
307
|
-
return
|
402
|
+
return redirect_to edit_admin_import_path(@import)
|
308
403
|
end
|
309
404
|
end
|
310
405
|
|
311
406
|
def update
|
312
407
|
@import = Import.find(params[:id])
|
313
408
|
@import.attributes = params[:import] || {}
|
409
|
+
return render :edit unless @import.save
|
314
410
|
|
315
411
|
if @import.import!
|
316
412
|
process_success
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
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.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Francis
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-01-
|
11
|
+
date: 2017-01-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|