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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4e0aed6e505dbece1acd37592260ff5348f95a69
4
- data.tar.gz: ee33de362f185bb1bf670712e0da5fbf5d22de69
3
+ metadata.gz: d03b574a8314c2eb5c192a7dcb2437cd93a6bf8b
4
+ data.tar.gz: c24aa7a22db05556ebe510e137d52cbd981ba4dc
5
5
  SHA512:
6
- metadata.gz: c8fa7d45452b176b052ab5d925fb82c51c23b72c1ead2fc6cd05d19db1bb6cd9461cc60bfd0798017061c98dda3a1332b55d8c8120e4757164850c353a81b48f
7
- data.tar.gz: 6b4e66526a3a0c84b632ee276c64d250821efc73a77cb67d377a78d597c489fe561b816e4ad642a41b75ff69e5edfd864b37c06eb5f598101a1745ac181b536b
6
+ metadata.gz: 59e81c752e193230d82568b113a084dfe97452a4d5d326c71cd6ed85f2afcb3cc1eb3d1be459690c8ba0a13269babe430d62ee2322b5d8cb36a2f315ac0a6e6e
7
+ data.tar.gz: 28a2c42b3545a1613adb6e41efade75bc9ea0267cdea9ca5f0154617b61800035cac9f2455a604fe4ecab1b8bfd695b9b2b0670a5d8ec594504804d10983b48c
data/README.md CHANGED
@@ -1,6 +1,26 @@
1
1
  # CSV Importable
2
2
 
3
- Intelligently parse CSVs and display errors to your users.
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
- # Paperclip.io_adapters.for(file).read
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::CSVImporter` and (2) implement the `import_row` method.
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::CSVImporter
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? See the Parsers section below for more information on how to take advantage of them.
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 of users for your new `UserImport`.
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, only: [:new, :create, :index]
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(params[:user_import])
152
+ @import = UserImport.new(user_import_params)
153
+ process_import
154
+ end
131
155
 
132
- if @import.import!
133
- redirect_to :back, notice: "The file is being imported."
134
- else
135
- render :new
136
- end
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('my_boolean_column', options: ['Yes', 'No'])`)
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 TypeParser::CustomDateTypeParser < CSVImportable::TypeParser
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: `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.
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
- f.inputs "Details" do
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 render :new
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
@@ -8,7 +8,10 @@ module CSVImportable
8
8
 
9
9
  def parse_val
10
10
  val = value.downcase
11
- raise unless options.include?(val)
11
+ opts = options.map do |option|
12
+ option.downcase if option.respond_to?(:downcase)
13
+ end
14
+ raise unless opts.include?(val)
12
15
  val
13
16
  end
14
17
 
@@ -1,3 +1,3 @@
1
1
  module CsvImportable
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.5"
3
3
  end
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
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-12 00:00:00.000000000 Z
11
+ date: 2017-01-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler