active_admin_import 2.1.2 → 3.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.hound.yml +4 -0
  3. data/.travis.yml +9 -0
  4. data/Gemfile +16 -0
  5. data/README.md +73 -173
  6. data/Rakefile +7 -2
  7. data/active_admin_import.gemspec +5 -3
  8. data/app/views/admin/import.html.erb +1 -1
  9. data/config/locales/en.yml +11 -4
  10. data/config/locales/es.yml +18 -0
  11. data/config/locales/it.yml +3 -1
  12. data/config/locales/zh-CN.yml +24 -0
  13. data/lib/active_admin_import/dsl.rb +39 -31
  14. data/lib/active_admin_import/import_result.rb +39 -0
  15. data/lib/active_admin_import/importer.rb +91 -44
  16. data/lib/active_admin_import/model.rb +80 -29
  17. data/lib/active_admin_import/options.rb +44 -0
  18. data/lib/active_admin_import/version.rb +1 -1
  19. data/lib/active_admin_import.rb +3 -0
  20. data/spec/fixtures/files/author.csv +2 -0
  21. data/spec/fixtures/files/author_broken_header.csv +2 -0
  22. data/spec/fixtures/files/author_invalid.csv +2 -0
  23. data/spec/fixtures/files/authors.csv +3 -0
  24. data/spec/fixtures/files/authors_bom.csv +3 -0
  25. data/spec/fixtures/files/authors_invalid_db.csv +3 -0
  26. data/spec/fixtures/files/authors_invalid_model.csv +3 -0
  27. data/spec/fixtures/files/authors_no_headers.csv +2 -0
  28. data/spec/fixtures/files/authors_win1251_win_endline.csv +3 -0
  29. data/spec/fixtures/files/authors_with_ids.csv +3 -0
  30. data/spec/fixtures/files/authors_with_semicolons.csv +3 -0
  31. data/spec/fixtures/files/empty.csv +0 -0
  32. data/spec/fixtures/files/only_headers.csv +1 -0
  33. data/spec/fixtures/files/posts.csv +4 -0
  34. data/spec/fixtures/files/posts_for_author.csv +3 -0
  35. data/spec/fixtures/files/posts_for_author_no_headers.csv +2 -0
  36. data/spec/import_result_spec.rb +32 -0
  37. data/spec/import_spec.rb +432 -0
  38. data/spec/model_spec.rb +5 -0
  39. data/spec/spec_helper.rb +72 -0
  40. data/spec/support/active_model_lint.rb +14 -0
  41. data/spec/support/admin.rb +20 -0
  42. data/spec/support/rails_template.rb +29 -0
  43. data/tasks/test.rake +6 -0
  44. metadata +80 -19
@@ -0,0 +1,432 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'import', type: :feature do
4
+
5
+ shared_examples 'successful inserts' do |encoding, csv_file_name|
6
+ let(:options) do
7
+ attributes = { force_encoding: encoding }
8
+ { template_object: ActiveAdminImport::Model.new(attributes) }
9
+ end
10
+
11
+ before do
12
+ upload_file!(csv_file_name)
13
+ end
14
+
15
+ it "should import file with many records" do
16
+ expect(page).to have_content "Successfully imported 2 authors"
17
+ expect(Author.count).to eq(2)
18
+ Author.all.each do |author|
19
+ expect(author).to be_valid
20
+ expect(author.name).to be_present
21
+ expect(author.last_name).to be_present
22
+ end
23
+ end
24
+ end
25
+
26
+ def with_zipped_csv(name, &block)
27
+
28
+ zip_file = File.expand_path("./spec/fixtures/files/#{name}.zip")
29
+
30
+ begin
31
+ Zip::File.open(zip_file, Zip::File::CREATE) do |z|
32
+ z.add "#{name}.csv", File.expand_path("./spec/fixtures/files/#{name}.csv")
33
+ end
34
+ instance_eval &block
35
+ ensure
36
+ File.delete zip_file rescue nil
37
+ end
38
+ end
39
+
40
+ def upload_file!(name, ext='csv')
41
+ attach_file('active_admin_import_model_file', File.expand_path("./spec/fixtures/files/#{name}.#{ext}"))
42
+ find_button('Import').click
43
+ end
44
+
45
+ context "posts index" do
46
+ before do
47
+ Author.create!(name: "John", last_name: "Doe")
48
+ Author.create!(name: "Jane", last_name: "Roe")
49
+ end
50
+
51
+ context "for csv for particular author" do
52
+ let(:author) { Author.take }
53
+
54
+ shared_examples 'successful inserts for author' do
55
+ it "should use predefined author_id" do
56
+ expect(Post.where(author_id: author.id).count).to eq(Post.count)
57
+ end
58
+
59
+ it "should be imported" do
60
+ expect(Post.count).to eq(2)
61
+ expect(page).to have_content "Successfully imported 2 posts"
62
+ end
63
+ end
64
+
65
+ context "no headers" do
66
+ before do
67
+ add_post_resource(template_object: ActiveAdminImport::Model.new(author_id: author.id,
68
+ csv_headers: [:title, :body, :author_id]),
69
+ validate: true,
70
+ before_batch_import: ->(importer) do
71
+ importer.csv_lines.map! { |row| row << importer.model.author_id }
72
+ end
73
+ )
74
+
75
+ visit "/admin/posts/import"
76
+ upload_file!(:posts_for_author_no_headers)
77
+ end
78
+ include_examples 'successful inserts for author'
79
+ end
80
+
81
+ context "with headers" do
82
+ before do
83
+ add_post_resource(template_object: ActiveAdminImport::Model.new(author_id: author.id),
84
+ validate: true,
85
+ before_batch_import: ->(importer) do
86
+ importer.csv_lines.map! { |row| row << importer.model.author_id }
87
+ importer.headers.merge!({ :'Author Id' => :author_id })
88
+ end
89
+ )
90
+
91
+ visit "/admin/posts/import"
92
+ upload_file!(:posts_for_author)
93
+ end
94
+ include_examples 'successful inserts for author'
95
+ end
96
+ end
97
+
98
+ context "for csv with author name" do
99
+ before do
100
+ add_post_resource(
101
+ validate: true,
102
+ template_object: ActiveAdminImport::Model.new,
103
+ headers_rewrites: { :'Author Name' => :author_id },
104
+ before_batch_import: ->(importer) do
105
+ authors_names = importer.values_at(:author_id)
106
+ # replacing author name with author id
107
+ authors = Author.where(name: authors_names).pluck(:name, :id)
108
+ #{"Jane" => 2, "John" => 1}
109
+ options = Hash[*authors.flatten]
110
+ importer.batch_replace(:author_id, options)
111
+ end
112
+ )
113
+ visit "/admin/posts/import"
114
+ upload_file!(:posts)
115
+ end
116
+
117
+ it "should resolve author_id by author name" do
118
+ Post.all.each do |post|
119
+ expect(Author.where(id: post.author.id)).to be_present
120
+ end
121
+ end
122
+
123
+ it "should be imported" do
124
+ expect(Post.count).to eq(2)
125
+ expect(page).to have_content "Successfully imported 2 posts"
126
+ end
127
+ end
128
+ end
129
+
130
+ context "authors index" do
131
+ before do
132
+ add_author_resource
133
+ end
134
+
135
+ it "should navigate to import page" do
136
+ #todo: removing this causes undefined method `ransack' for #<ActiveRecord::Relation []>
137
+ allow_any_instance_of(Admin::AuthorsController).to receive(:find_collection).and_return(Author.all)
138
+ visit '/admin/authors'
139
+ find_link('Import Authors').click
140
+ expect(current_path).to eq("/admin/authors/import")
141
+ end
142
+ end
143
+
144
+ context "with custom block" do
145
+ before do
146
+ add_author_resource({}) do
147
+ flash[:notice] = 'some custom message'
148
+ end
149
+ visit '/admin/authors/import'
150
+ end
151
+
152
+ it "should display notice from custom block" do
153
+ upload_file!(:author)
154
+ expect(page).to have_content "some custom message"
155
+ end
156
+
157
+ end
158
+
159
+ context "authors already exist" do
160
+ before do
161
+ Author.create!(id: 1, name: "Jane", last_name: "Roe")
162
+ Author.create!(id: 2, name: "John", last_name: "Doe")
163
+ end
164
+
165
+ context "having authors with the same Id" do
166
+ before do
167
+ add_author_resource(
168
+ before_batch_import: ->(importer) do
169
+ Author.where(id: importer.values_at("id")).delete_all
170
+ end
171
+ )
172
+ visit "/admin/authors/import"
173
+ upload_file!(:authors_with_ids)
174
+ end
175
+
176
+ it "should replace authors" do
177
+ expect(page).to have_content "Successfully imported 2 authors"
178
+ expect(Author.count).to eq(2)
179
+ end
180
+
181
+ it "should replace authors by id" do
182
+ expect(Author.find(1).name).to eq("John")
183
+ expect(Author.find(2).name).to eq("Jane")
184
+ end
185
+ end
186
+ end
187
+
188
+ context "with valid options" do
189
+
190
+ let(:options) { {} }
191
+
192
+ before do
193
+ add_author_resource(options)
194
+ visit '/admin/authors/import'
195
+ end
196
+
197
+ it "has valid form" do
198
+ form = find('#new_active_admin_import_model')
199
+ expect(form['action']).to eq("/admin/authors/do_import")
200
+ expect(form['enctype']).to eq("multipart/form-data")
201
+ file_input = form.find("input#active_admin_import_model_file")
202
+ expect(file_input[:type]).to eq("file")
203
+ expect(file_input.value).to be_blank
204
+ submit_input = form.find("#active_admin_import_model_submit_action input")
205
+ expect(submit_input[:value]).to eq("Import")
206
+ expect(submit_input[:type]).to eq("submit")
207
+ end
208
+
209
+ context "with hint defined" do
210
+ let(:options) do
211
+ { template_object: ActiveAdminImport::Model.new(hint: "hint") }
212
+ end
213
+ it "renders hint at upload page" do
214
+ expect(page).to have_content options[:template_object].hint
215
+ end
216
+ end
217
+
218
+ context "when importing file" do
219
+
220
+ [:empty, :only_headers].each do |file|
221
+ context "when #{file} file" do
222
+ it "should render warning" do
223
+ upload_file!(file)
224
+ expect(page).to have_content I18n.t('active_admin_import.file_empty_error')
225
+ expect(Author.count).to eq(0)
226
+ end
227
+ end
228
+ end
229
+
230
+ context "when no file" do
231
+ it "should render error" do
232
+ find_button('Import').click
233
+ expect(Author.count).to eq(0)
234
+ expect(page).to have_content I18n.t('active_admin_import.no_file_error')
235
+ end
236
+ end
237
+
238
+ context "auto detect encoding" do
239
+ include_examples 'successful inserts',
240
+ :auto,
241
+ :authors_win1251_win_endline
242
+ end
243
+
244
+ context "Win1251" do
245
+ include_examples 'successful inserts',
246
+ 'windows-1251',
247
+ :authors_win1251_win_endline
248
+ end
249
+
250
+ context "BOM" do
251
+ it "should import file with many records" do
252
+ upload_file!(:authors_bom)
253
+ expect(page).to have_content "Successfully imported 2 authors"
254
+ expect(Author.count).to eq(2)
255
+ end
256
+ end
257
+
258
+ context "with headers" do
259
+ it "should import file with many records" do
260
+ upload_file!(:authors)
261
+ expect(page).to have_content "Successfully imported 2 authors"
262
+ expect(Author.count).to eq(2)
263
+ end
264
+
265
+ it "should import file with 1 record" do
266
+ upload_file!(:author)
267
+ expect(page).to have_content "Successfully imported 1 author"
268
+ expect(Author.count).to eq(1)
269
+ end
270
+ end
271
+
272
+ context "without headers" do
273
+ context "with known csv headers" do
274
+ let(:options) do
275
+ attributes = { csv_headers: ['Name', 'Last name', 'Birthday'] }
276
+ { template_object: ActiveAdminImport::Model.new(attributes) }
277
+ end
278
+
279
+ it "should import file" do
280
+ upload_file!(:authors_no_headers)
281
+ expect(page).to have_content "Successfully imported 2 authors"
282
+ expect(Author.count).to eq(2)
283
+ end
284
+ end
285
+
286
+ context "with unknown csv headers" do
287
+ it "should render error" do
288
+ upload_file!(:authors_no_headers)
289
+ expect(page).to have_content "Error:"
290
+ expect(Author.count).to eq(0)
291
+ end
292
+ end
293
+ end
294
+
295
+ context "with invalid data insert on DB constraint" do
296
+ # :name field has an uniq index
297
+ it "should render error" do
298
+ upload_file!(:authors_invalid_db)
299
+ expect(page).to have_content "Error:"
300
+ expect(Author.count).to eq(0)
301
+ end
302
+ end
303
+
304
+ context "with invalid data insert on model validation" do
305
+ let(:options) { { validate: true } }
306
+
307
+ before do
308
+ Author.create!(name: "John", last_name: "Doe")
309
+ end
310
+
311
+ it "should render both successful and failed message" do
312
+ upload_file!(:authors_invalid_model)
313
+ expect(page).to have_content "Failed to import 1 author"
314
+ expect(page).to have_content "Successfully imported 1 author"
315
+ expect(Author.count).to eq(2)
316
+ end
317
+
318
+ context "use batch_transaction to make transaction work on model validation" do
319
+ let(:options) { { validate: true, batch_transaction: true } }
320
+
321
+ it "should render only the failed message" do
322
+ upload_file!(:authors_invalid_model)
323
+ expect(page).to have_content "Failed to import 1 author"
324
+ expect(page).to_not have_content "Successfully imported"
325
+ expect(Author.count).to eq(1)
326
+ end
327
+ end
328
+ end
329
+
330
+ context "with invalid records" do
331
+ context "with validation" do
332
+ it "should render error" do
333
+ upload_file!(:author_invalid)
334
+ expect(page).to have_content "Failed to import 1 author"
335
+ expect(Author.count).to eq(0)
336
+ end
337
+ end
338
+
339
+ context "without validation" do
340
+ let(:options) { { validate: false } }
341
+ it "should render error" do
342
+ upload_file!(:author_invalid)
343
+ expect(page).to have_content "Successfully imported 1 author"
344
+ expect(Author.count).to eq(1)
345
+ end
346
+ end
347
+ end
348
+
349
+ context "when zipped" do
350
+ context "when allowed" do
351
+ it "should import file" do
352
+ with_zipped_csv(:authors) do
353
+ upload_file!(:authors, :zip)
354
+ expect(page).to have_content "Successfully imported 2 authors"
355
+ expect(Author.count).to eq(2)
356
+ end
357
+ end
358
+ end
359
+
360
+ context "when not allowed" do
361
+ let(:options) do
362
+ attributes = { allow_archive: false }
363
+ { template_object: ActiveAdminImport::Model.new(attributes) }
364
+ end
365
+ it "should render error" do
366
+ with_zipped_csv(:authors) do
367
+ upload_file!(:authors, :zip)
368
+ expect(page).to have_content I18n.t('active_admin_import.file_format_error')
369
+ expect(Author.count).to eq(0)
370
+ end
371
+ end
372
+ end
373
+ end
374
+
375
+ context "with different header attribute names" do
376
+ let(:options) do
377
+ { headers_rewrites: { :'Second name' => :last_name } }
378
+ end
379
+
380
+ it "should import file" do
381
+ upload_file!(:author_broken_header)
382
+ expect(page).to have_content "Successfully imported 1 author"
383
+ expect(Author.count).to eq(1)
384
+ end
385
+ end
386
+
387
+ context "with semicolons separator" do
388
+ let(:options) do
389
+ attributes = { csv_options: { col_sep: ";" } }
390
+ { template_object: ActiveAdminImport::Model.new(attributes) }
391
+ end
392
+
393
+ it "should import file" do
394
+ upload_file!(:authors_with_semicolons)
395
+ expect(page).to have_content "Successfully imported 2 authors"
396
+ expect(Author.count).to eq(2)
397
+ end
398
+ end
399
+ end
400
+
401
+ context "with callback procs options" do
402
+ let(:options) do
403
+ {
404
+ before_import: ->(_) { true },
405
+ after_import: ->(_) { true },
406
+ before_batch_import: ->(_) { true },
407
+ after_batch_import: ->(_) { true }
408
+ }
409
+ end
410
+
411
+ it "should call each callback" do
412
+ expect(options[:before_import]).to receive(:call).with(kind_of(ActiveAdminImport::Importer))
413
+ expect(options[:after_import]).to receive(:call).with(kind_of(ActiveAdminImport::Importer))
414
+ expect(options[:before_batch_import]).to receive(:call).with(kind_of(ActiveAdminImport::Importer))
415
+ expect(options[:after_batch_import]).to receive(:call).with(kind_of(ActiveAdminImport::Importer))
416
+ upload_file!(:authors)
417
+ expect(Author.count).to eq(2)
418
+ end
419
+ end
420
+
421
+ end
422
+
423
+ context "with invalid options" do
424
+ let(:options) { { invalid_option: :invalid_value } }
425
+
426
+ it "should raise TypeError" do
427
+ expect { add_author_resource(options) }.to raise_error(ArgumentError)
428
+ end
429
+
430
+ end
431
+
432
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+ require 'active_model_lint'
3
+ describe ActiveAdminImport::Model do
4
+ it_behaves_like "ActiveModel"
5
+ end
@@ -0,0 +1,72 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ $LOAD_PATH << File.expand_path('../support', __FILE__)
6
+
7
+ ENV['BUNDLE_GEMFILE'] = File.expand_path('../../Gemfile', __FILE__)
8
+ require "bundler"
9
+ Bundler.setup
10
+
11
+ ENV['RAILS'] = '4.1.9'
12
+
13
+ ENV['RAILS_ENV'] = 'test'
14
+ ENV['RAILS_ROOT'] = File.expand_path("../rails/rails-#{ENV['RAILS']}", __FILE__)
15
+
16
+ # Create the test app if it doesn't exists
17
+ unless File.exists?(ENV['RAILS_ROOT'])
18
+ system 'rake setup'
19
+ end
20
+
21
+ # Ensure the Active Admin load path is happy
22
+ require 'rails'
23
+ require 'active_model'
24
+ require 'active_admin'
25
+ ActiveAdmin.application.load_paths = [ENV['RAILS_ROOT'] + "/app/admin"]
26
+
27
+ require ENV['RAILS_ROOT'] + '/config/environment.rb'
28
+
29
+ # Disabling authentication in specs so that we don't have to worry about
30
+ # it allover the place
31
+ ActiveAdmin.application.authentication_method = false
32
+ ActiveAdmin.application.current_user_method = false
33
+
34
+ require 'rspec/rails'
35
+ require 'support/admin'
36
+ require 'capybara/rails'
37
+ require 'capybara/rspec'
38
+ require 'capybara/poltergeist'
39
+
40
+
41
+
42
+
43
+ Capybara.register_driver :poltergeist do |app|
44
+ Capybara::Poltergeist::Driver.new(app, {
45
+ js_errors: true,
46
+ timeout: 80,
47
+ debug: true,
48
+ :phantomjs_options => ['--debug=no', '--load-images=no']
49
+
50
+ })
51
+ end
52
+
53
+
54
+ Capybara.javascript_driver = :poltergeist
55
+
56
+
57
+ RSpec.configure do |config|
58
+ config.use_transactional_fixtures = false
59
+
60
+ config.before(:suite) do
61
+ DatabaseCleaner.strategy = :truncation
62
+ DatabaseCleaner.clean_with(:truncation)
63
+ end
64
+ config.before(:each) do
65
+ DatabaseCleaner.strategy = :truncation
66
+ DatabaseCleaner.start
67
+ end
68
+ config.after(:each) do
69
+ DatabaseCleaner.clean
70
+ end
71
+ end
72
+
@@ -0,0 +1,14 @@
1
+ shared_examples_for "ActiveModel" do
2
+ include ActiveModel::Lint::Tests
3
+
4
+ # to_s is to support ruby-1.9
5
+ ActiveModel::Lint::Tests.public_instance_methods.map{|m| m.to_s}.grep(/^test/).each do |m|
6
+ example m.gsub('_',' ') do
7
+ send m
8
+ end
9
+ end
10
+
11
+ def model
12
+ subject
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ def add_author_resource(options = {}, &block)
2
+
3
+ ActiveAdmin.register Author do
4
+ config.filters = false
5
+ active_admin_import(options, &block)
6
+ end
7
+ Rails.application.reload_routes!
8
+
9
+ end
10
+
11
+
12
+ def add_post_resource(options = {}, &block)
13
+
14
+ ActiveAdmin.register Post do
15
+ config.filters = false
16
+ active_admin_import(options, &block)
17
+ end
18
+ Rails.application.reload_routes!
19
+
20
+ end
@@ -0,0 +1,29 @@
1
+ # Rails template to build the sample app for specs
2
+
3
+ generate :model, 'author name:string{10}:uniq last_name:string birthday:date'
4
+ generate :model, 'post title:string:uniq body:text author:references'
5
+
6
+ #Add validation
7
+ inject_into_file "app/models/author.rb", " validates_presence_of :name\n validates_uniqueness_of :last_name\n", after: "Base\n"
8
+ inject_into_file "app/models/post.rb", " validates_presence_of :author\n", after: ":author\n"
9
+
10
+ # Configure default_url_options in test environment
11
+ inject_into_file "config/environments/test.rb", " config.action_mailer.default_url_options = { :host => 'example.com' }\n", after: "config.cache_classes = true\n"
12
+
13
+ # Add our local Active Admin to the load path
14
+ inject_into_file "config/environment.rb", "\n$LOAD_PATH.unshift('#{File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib'))}')\nrequire \"active_admin\"\n", after: "require File.expand_path('../application', __FILE__)"
15
+
16
+ run "rm Gemfile"
17
+
18
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
19
+
20
+ generate :'active_admin:install --skip-users'
21
+ generate :'formtastic:install'
22
+
23
+ run "rm -r test"
24
+ run "rm -r spec"
25
+
26
+ # Setup a root path for devise
27
+ route "root :to => 'admin/dashboard#index'"
28
+
29
+ rake "db:migrate"
data/tasks/test.rake ADDED
@@ -0,0 +1,6 @@
1
+ desc "Creates a test rails app for the specs to run against"
2
+ task :setup do
3
+ require 'rails/version'
4
+ system("mkdir spec/rails") unless File.exists?("spec/rails")
5
+ system "bundle exec rails new spec/rails/rails-#{Rails::VERSION::STRING} -m spec/support/rails_template.rb --skip-spring"
6
+ end