active_admin_import 2.1.2 → 3.0.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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