active_admin_import 8.0.0 → 8.0.1
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.
- checksums.yaml +4 -4
- data/lib/active_admin_import/version.rb +1 -1
- metadata +2 -42
- data/.github/workflows/test.yml +0 -219
- data/.gitignore +0 -19
- data/.rubocop.yml +0 -43
- data/Gemfile +0 -37
- data/Rakefile +0 -5
- data/active_admin_import.gemspec +0 -22
- data/spec/fixtures/files/author.csv +0 -2
- data/spec/fixtures/files/author_broken_header.csv +0 -2
- data/spec/fixtures/files/author_invalid.csv +0 -2
- data/spec/fixtures/files/author_invalid_format.txt +0 -2
- data/spec/fixtures/files/authors.csv +0 -3
- data/spec/fixtures/files/authors_blank_header_end.csv +0 -3
- data/spec/fixtures/files/authors_blank_header_middle.csv +0 -3
- data/spec/fixtures/files/authors_bom.csv +0 -3
- data/spec/fixtures/files/authors_empty_header.csv +0 -3
- data/spec/fixtures/files/authors_invalid_db.csv +0 -3
- data/spec/fixtures/files/authors_invalid_model.csv +0 -3
- data/spec/fixtures/files/authors_many.csv +0 -6
- data/spec/fixtures/files/authors_no_headers.csv +0 -2
- data/spec/fixtures/files/authors_values_exceeded_headers.csv +0 -3
- data/spec/fixtures/files/authors_win1251_win_endline.csv +0 -3
- data/spec/fixtures/files/authors_with_ids.csv +0 -3
- data/spec/fixtures/files/authors_with_semicolons.csv +0 -3
- data/spec/fixtures/files/authors_with_tabs.tsv +0 -3
- data/spec/fixtures/files/empty.csv +0 -0
- data/spec/fixtures/files/only_headers.csv +0 -1
- data/spec/fixtures/files/post_comments.csv +0 -3
- data/spec/fixtures/files/posts.csv +0 -4
- data/spec/fixtures/files/posts_for_author.csv +0 -3
- data/spec/fixtures/files/posts_for_author_no_headers.csv +0 -2
- data/spec/import_result_spec.rb +0 -39
- data/spec/import_spec.rb +0 -859
- data/spec/model_spec.rb +0 -6
- data/spec/spec_helper.rb +0 -54
- data/spec/support/active_model_lint.rb +0 -15
- data/spec/support/admin.rb +0 -32
- data/spec/support/import_form_selectors.rb +0 -24
- data/spec/support/rails_template.rb +0 -71
- data/spec/support/test_app_paths.rb +0 -14
- data/tasks/test.rake +0 -39
data/spec/import_spec.rb
DELETED
|
@@ -1,859 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
require 'spec_helper'
|
|
3
|
-
|
|
4
|
-
describe 'import', type: :feature do
|
|
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
|
-
zip_file = File.expand_path("./spec/fixtures/files/#{name}.zip")
|
|
28
|
-
|
|
29
|
-
begin
|
|
30
|
-
Zip::File.open(zip_file, create: true) do |z|
|
|
31
|
-
z.add "#{name}.csv", File.expand_path("./spec/fixtures/files/#{name}.csv")
|
|
32
|
-
end
|
|
33
|
-
instance_eval &block
|
|
34
|
-
ensure
|
|
35
|
-
begin
|
|
36
|
-
File.delete zip_file
|
|
37
|
-
rescue
|
|
38
|
-
nil
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def upload_file!(name, ext = 'csv')
|
|
44
|
-
attach_file(ImportFormSelectors.file_input_id, File.expand_path("./spec/fixtures/files/#{name}.#{ext}"))
|
|
45
|
-
find_button(ImportFormSelectors.import_button_text).click
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
context 'posts index' do
|
|
49
|
-
before do
|
|
50
|
-
Author.create!(name: 'John', last_name: 'Doe')
|
|
51
|
-
Author.create!(name: 'Jane', last_name: 'Roe')
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
context 'for csv for particular author' do
|
|
55
|
-
let(:author) { Author.take }
|
|
56
|
-
|
|
57
|
-
shared_examples 'successful inserts for author' do
|
|
58
|
-
it 'should use predefined author_id' do
|
|
59
|
-
expect(Post.where(author_id: author.id).count).to eq(Post.count)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
it 'should be imported' do
|
|
63
|
-
expect(Post.count).to eq(2)
|
|
64
|
-
expect(page).to have_content 'Successfully imported 2 posts'
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
context 'no headers' do
|
|
69
|
-
before do
|
|
70
|
-
add_post_resource(template_object: ActiveAdminImport::Model.new(author_id: author.id,
|
|
71
|
-
csv_headers: [:title, :body, :author_id]),
|
|
72
|
-
validate: true,
|
|
73
|
-
before_batch_import: lambda do |importer|
|
|
74
|
-
importer.csv_lines.map! { |row| row << importer.model.author_id }
|
|
75
|
-
end
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
visit '/admin/posts/import'
|
|
79
|
-
upload_file!(:posts_for_author_no_headers)
|
|
80
|
-
end
|
|
81
|
-
include_examples 'successful inserts for author'
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
context 'with headers' do
|
|
85
|
-
before do
|
|
86
|
-
add_post_resource(template_object: ActiveAdminImport::Model.new(author_id: author.id),
|
|
87
|
-
validate: true,
|
|
88
|
-
before_batch_import: lambda do |importer|
|
|
89
|
-
importer.csv_lines.map! { |row| row << importer.model.author_id }
|
|
90
|
-
importer.headers.merge!(:'Author Id' => :author_id)
|
|
91
|
-
end
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
visit '/admin/posts/import'
|
|
95
|
-
upload_file!(:posts_for_author)
|
|
96
|
-
end
|
|
97
|
-
include_examples 'successful inserts for author'
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
context 'when template object passed like proc' do
|
|
101
|
-
before do
|
|
102
|
-
add_post_resource(template_object: -> { ActiveAdminImport::Model.new(author_id: author.id) },
|
|
103
|
-
validate: true,
|
|
104
|
-
before_batch_import: lambda do |importer|
|
|
105
|
-
importer.csv_lines.map! { |row| row << importer.model.author_id }
|
|
106
|
-
importer.headers.merge!(:'Author Id' => :author_id)
|
|
107
|
-
end
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
visit '/admin/posts/import'
|
|
111
|
-
upload_file!(:posts_for_author)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
include_examples 'successful inserts for author'
|
|
115
|
-
|
|
116
|
-
context 'after successful import try without file' do
|
|
117
|
-
let(:after_successful_import_do!) do
|
|
118
|
-
# reload page
|
|
119
|
-
visit '/admin/posts/import'
|
|
120
|
-
# submit form without file
|
|
121
|
-
find_button(ImportFormSelectors.import_button_text).click
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
it 'should render validation error' do
|
|
125
|
-
after_successful_import_do!
|
|
126
|
-
|
|
127
|
-
expect(page).to have_content I18n.t('active_admin_import.no_file_error')
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
context 'for csv with author name' do
|
|
134
|
-
before do
|
|
135
|
-
add_post_resource(
|
|
136
|
-
validate: true,
|
|
137
|
-
template_object: ActiveAdminImport::Model.new,
|
|
138
|
-
headers_rewrites: { :'Author Name' => :author_id },
|
|
139
|
-
before_batch_import: lambda do |importer|
|
|
140
|
-
authors_names = importer.values_at(:author_id)
|
|
141
|
-
# replacing author name with author id
|
|
142
|
-
authors = Author.where(name: authors_names).pluck(:name, :id)
|
|
143
|
-
# {"Jane" => 2, "John" => 1}
|
|
144
|
-
options = Hash[*authors.flatten]
|
|
145
|
-
importer.batch_replace(:author_id, options)
|
|
146
|
-
end
|
|
147
|
-
)
|
|
148
|
-
visit '/admin/posts/import'
|
|
149
|
-
upload_file!(:posts)
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
it 'should resolve author_id by author name' do
|
|
153
|
-
Post.all.each do |post|
|
|
154
|
-
expect(Author.where(id: post.author.id)).to be_present
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
it 'should be imported' do
|
|
159
|
-
expect(Post.count).to eq(2)
|
|
160
|
-
expect(page).to have_content 'Successfully imported 2 posts'
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
context 'authors index' do
|
|
166
|
-
before do
|
|
167
|
-
add_author_resource
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
it 'should navigate to import page' do
|
|
171
|
-
# TODO: removing this causes undefined method `ransack' for #<ActiveRecord::Relation []>
|
|
172
|
-
allow_any_instance_of(Admin::AuthorsController).to receive(:find_collection).and_return(Author.all)
|
|
173
|
-
visit '/admin/authors'
|
|
174
|
-
find_link(ImportFormSelectors.import_link_text).click
|
|
175
|
-
expect(current_path).to eq('/admin/authors/import')
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
context 'with custom block' do
|
|
180
|
-
before do
|
|
181
|
-
add_author_resource({}) do
|
|
182
|
-
flash[:notice] = 'some custom message'
|
|
183
|
-
end
|
|
184
|
-
visit '/admin/authors/import'
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
it 'should display notice from custom block' do
|
|
188
|
-
upload_file!(:author)
|
|
189
|
-
expect(page).to have_content 'some custom message'
|
|
190
|
-
end
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
# Issue #197: a CSV with an empty header cell used to crash with
|
|
194
|
-
# `undefined method 'underscore' for nil`, wherever the blank sits.
|
|
195
|
-
context 'with a blank column header' do
|
|
196
|
-
shared_examples 'a rejected blank header' do |fixture, column|
|
|
197
|
-
before do
|
|
198
|
-
add_author_resource
|
|
199
|
-
visit '/admin/authors/import'
|
|
200
|
-
upload_file!(fixture)
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
it 'reports a clear error and imports nothing' do
|
|
204
|
-
expect(page).to have_content "blank column header at column #{column}"
|
|
205
|
-
expect(page).to have_no_content 'undefined method'
|
|
206
|
-
expect(Author.count).to eq(0)
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
context 'at the beginning' do
|
|
211
|
-
it_behaves_like 'a rejected blank header', :authors_empty_header, 1
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
context 'in the middle' do
|
|
215
|
-
it_behaves_like 'a rejected blank header', :authors_blank_header_middle, 2
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
context 'at the end' do
|
|
219
|
-
it_behaves_like 'a rejected blank header', :authors_blank_header_end, 4
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
context 'authors already exist' do
|
|
224
|
-
before do
|
|
225
|
-
Author.create!(id: 1, name: 'Jane', last_name: 'Roe')
|
|
226
|
-
Author.create!(id: 2, name: 'John', last_name: 'Doe')
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
context 'having authors with the same Id' do
|
|
230
|
-
before do
|
|
231
|
-
add_author_resource(
|
|
232
|
-
before_batch_import: lambda do |importer|
|
|
233
|
-
Author.where(id: importer.values_at('id')).delete_all
|
|
234
|
-
end
|
|
235
|
-
)
|
|
236
|
-
visit '/admin/authors/import'
|
|
237
|
-
upload_file!(:authors_with_ids)
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
it 'should replace authors' do
|
|
241
|
-
expect(page).to have_content 'Successfully imported 2 authors'
|
|
242
|
-
expect(Author.count).to eq(2)
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
it 'should replace authors by id' do
|
|
246
|
-
expect(Author.find(1).name).to eq('John')
|
|
247
|
-
expect(Author.find(2).name).to eq('Jane')
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
# Issue #187: update existing records by id without a delete_all workaround.
|
|
252
|
-
# On databases that support upserts, :on_duplicate_key_update lets a single
|
|
253
|
-
# import update colliding rows and insert new ones in one pass.
|
|
254
|
-
context 'upserting authors by id via :on_duplicate_key_update' do
|
|
255
|
-
before do
|
|
256
|
-
# Existing row shares its id with the first CSV row but carries a stale
|
|
257
|
-
# birthday; the second CSV row (id 2) has no match and must be inserted.
|
|
258
|
-
Author.delete_all
|
|
259
|
-
Author.create!(id: 1, name: 'John', last_name: 'Doe', birthday: '1900-01-01')
|
|
260
|
-
|
|
261
|
-
# The option shape is adapter-specific: MySQL infers the conflicting key
|
|
262
|
-
# and only wants the column list, while PostgreSQL/SQLite need an explicit
|
|
263
|
-
# :conflict_target (see README).
|
|
264
|
-
on_duplicate_key_update =
|
|
265
|
-
if ActiveRecord::Base.connection.adapter_name.match?(/mysql/i)
|
|
266
|
-
%i[name last_name birthday]
|
|
267
|
-
else
|
|
268
|
-
{ conflict_target: [:id], columns: %i[name last_name birthday] }
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
add_author_resource(
|
|
272
|
-
# Uniqueness validation runs against the rows the upsert is about to
|
|
273
|
-
# overwrite, so it must be off for an id-based upsert (see README).
|
|
274
|
-
validate: false,
|
|
275
|
-
on_duplicate_key_update: on_duplicate_key_update
|
|
276
|
-
)
|
|
277
|
-
visit '/admin/authors/import'
|
|
278
|
-
upload_file!(:authors_with_ids)
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
it 'reports every row as imported' do
|
|
282
|
-
expect(page).to have_content 'Successfully imported 2 authors'
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
it 'updates the existing author instead of duplicating it' do
|
|
286
|
-
expect(Author.count).to eq(2)
|
|
287
|
-
expect(Author.find(1).birthday).to eq(Date.new(1986, 5, 1))
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
it 'inserts the non-colliding author' do
|
|
291
|
-
expect(Author.find(2).name).to eq('Jane')
|
|
292
|
-
expect(Author.find(2).last_name).to eq('Roe')
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
context 'with valid options' do
|
|
298
|
-
let(:options) { {} }
|
|
299
|
-
|
|
300
|
-
before do
|
|
301
|
-
add_author_resource(options)
|
|
302
|
-
visit '/admin/authors/import'
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
it 'has valid form' do
|
|
306
|
-
form = find(ImportFormSelectors.form_css)
|
|
307
|
-
expect(form['action']).to eq('/admin/authors/do_import')
|
|
308
|
-
expect(form['enctype']).to eq('multipart/form-data')
|
|
309
|
-
file_input = form.find(ImportFormSelectors.file_input_css)
|
|
310
|
-
expect(file_input[:type]).to eq('file')
|
|
311
|
-
expect(file_input.value).to be_blank
|
|
312
|
-
submit_input = form.find(ImportFormSelectors.submit_css)
|
|
313
|
-
expect(submit_input[:value]).to eq(ImportFormSelectors.import_button_text)
|
|
314
|
-
expect(submit_input[:type]).to eq('submit')
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
context 'with hint defined' do
|
|
318
|
-
let(:options) do
|
|
319
|
-
{ template_object: ActiveAdminImport::Model.new(hint: 'hint') }
|
|
320
|
-
end
|
|
321
|
-
it 'renders hint at upload page' do
|
|
322
|
-
expect(page).to have_content options[:template_object].hint
|
|
323
|
-
end
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
context 'when importing file' do
|
|
327
|
-
[:empty, :only_headers].each do |file|
|
|
328
|
-
context "when #{file} file" do
|
|
329
|
-
it 'should render warning' do
|
|
330
|
-
upload_file!(file)
|
|
331
|
-
expect(page).to have_content I18n.t('active_admin_import.file_empty_error')
|
|
332
|
-
expect(Author.count).to eq(0)
|
|
333
|
-
end
|
|
334
|
-
end
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
context 'when no file' do
|
|
338
|
-
it 'should render error' do
|
|
339
|
-
find_button(ImportFormSelectors.import_button_text).click
|
|
340
|
-
expect(Author.count).to eq(0)
|
|
341
|
-
expect(page).to have_content I18n.t('active_admin_import.no_file_error')
|
|
342
|
-
end
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
context 'auto detect encoding' do
|
|
346
|
-
include_examples 'successful inserts',
|
|
347
|
-
:auto,
|
|
348
|
-
:authors_win1251_win_endline
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
context 'Win1251' do
|
|
352
|
-
include_examples 'successful inserts',
|
|
353
|
-
'windows-1251',
|
|
354
|
-
:authors_win1251_win_endline
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
context 'BOM' do
|
|
358
|
-
it 'should import file with many records' do
|
|
359
|
-
upload_file!(:authors_bom)
|
|
360
|
-
expect(page).to have_content 'Successfully imported 2 authors'
|
|
361
|
-
expect(Author.count).to eq(2)
|
|
362
|
-
end
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
context 'with headers' do
|
|
366
|
-
it 'should import file with many records' do
|
|
367
|
-
upload_file!(:authors)
|
|
368
|
-
expect(page).to have_content 'Successfully imported 2 authors'
|
|
369
|
-
expect(Author.count).to eq(2)
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
it 'should import file with 1 record' do
|
|
373
|
-
upload_file!(:author)
|
|
374
|
-
expect(page).to have_content 'Successfully imported 1 author'
|
|
375
|
-
expect(Author.count).to eq(1)
|
|
376
|
-
end
|
|
377
|
-
end
|
|
378
|
-
|
|
379
|
-
context 'without headers' do
|
|
380
|
-
context 'with known csv headers' do
|
|
381
|
-
let(:options) do
|
|
382
|
-
attributes = { csv_headers: ['Name', 'Last name', 'Birthday'] }
|
|
383
|
-
{ template_object: ActiveAdminImport::Model.new(attributes) }
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
it 'should import file' do
|
|
387
|
-
upload_file!(:authors_no_headers)
|
|
388
|
-
expect(page).to have_content 'Successfully imported 2 authors'
|
|
389
|
-
expect(Author.count).to eq(2)
|
|
390
|
-
end
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
context 'with unknown csv headers' do
|
|
394
|
-
it 'should render error' do
|
|
395
|
-
upload_file!(:authors_no_headers)
|
|
396
|
-
expect(page).to have_content 'Error:'
|
|
397
|
-
expect(Author.count).to eq(0)
|
|
398
|
-
end
|
|
399
|
-
end
|
|
400
|
-
end
|
|
401
|
-
|
|
402
|
-
context 'with invalid data insert on DB constraint' do
|
|
403
|
-
# :name field has an uniq index
|
|
404
|
-
it 'should render error' do
|
|
405
|
-
upload_file!(:authors_invalid_db)
|
|
406
|
-
expect(page).to have_content 'Error:'
|
|
407
|
-
expect(Author.count).to eq(0)
|
|
408
|
-
end
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
context 'with invalid data insert on model validation' do
|
|
412
|
-
let(:options) { { validate: true } }
|
|
413
|
-
|
|
414
|
-
before do
|
|
415
|
-
Author.create!(name: 'John', last_name: 'Doe')
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
it 'should render both successful and failed message' do
|
|
419
|
-
upload_file!(:authors_invalid_model)
|
|
420
|
-
expect(page).to have_content 'Failed to import 1 author'
|
|
421
|
-
expect(page).to have_content 'Successfully imported 1 author'
|
|
422
|
-
expect(page).to have_content 'Last name has already been taken - Doe'
|
|
423
|
-
expect(Author.count).to eq(2)
|
|
424
|
-
end
|
|
425
|
-
|
|
426
|
-
context 'use batch_transaction to make transaction work on model validation' do
|
|
427
|
-
let(:options) { { validate: true, batch_transaction: true } }
|
|
428
|
-
|
|
429
|
-
it 'should render only the failed message' do
|
|
430
|
-
upload_file!(:authors_invalid_model)
|
|
431
|
-
expect(page).to have_content 'Failed to import 1 author'
|
|
432
|
-
expect(page).to_not have_content 'Successfully imported'
|
|
433
|
-
expect(Author.count).to eq(1)
|
|
434
|
-
end
|
|
435
|
-
end
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
context 'with invalid records' do
|
|
439
|
-
context 'with validation' do
|
|
440
|
-
it 'should render error' do
|
|
441
|
-
upload_file!(:author_invalid)
|
|
442
|
-
expect(page).to have_content 'Failed to import 1 author'
|
|
443
|
-
expect(Author.count).to eq(0)
|
|
444
|
-
end
|
|
445
|
-
end
|
|
446
|
-
|
|
447
|
-
context 'without validation' do
|
|
448
|
-
let(:options) { { validate: false } }
|
|
449
|
-
it 'should render error' do
|
|
450
|
-
upload_file!(:author_invalid)
|
|
451
|
-
expect(page).to have_content 'Successfully imported 1 author'
|
|
452
|
-
expect(Author.count).to eq(1)
|
|
453
|
-
end
|
|
454
|
-
end
|
|
455
|
-
end
|
|
456
|
-
|
|
457
|
-
context 'when zipped' do
|
|
458
|
-
context 'when allowed' do
|
|
459
|
-
it 'should import file' do
|
|
460
|
-
with_zipped_csv(:authors) do
|
|
461
|
-
upload_file!(:authors, :zip)
|
|
462
|
-
expect(page).to have_content 'Successfully imported 2 authors'
|
|
463
|
-
expect(Author.count).to eq(2)
|
|
464
|
-
end
|
|
465
|
-
end
|
|
466
|
-
end
|
|
467
|
-
|
|
468
|
-
context 'when not allowed' do
|
|
469
|
-
let(:options) do
|
|
470
|
-
attributes = { allow_archive: false }
|
|
471
|
-
{ template_object: ActiveAdminImport::Model.new(attributes) }
|
|
472
|
-
end
|
|
473
|
-
it 'should render error' do
|
|
474
|
-
with_zipped_csv(:authors) do
|
|
475
|
-
upload_file!(:authors, :zip)
|
|
476
|
-
expect(page).to have_content I18n.t('active_admin_import.file_format_error')
|
|
477
|
-
expect(Author.count).to eq(0)
|
|
478
|
-
end
|
|
479
|
-
end
|
|
480
|
-
end
|
|
481
|
-
|
|
482
|
-
context 'when zipped with Win1251 file' do
|
|
483
|
-
let(:options) do
|
|
484
|
-
attributes = { force_encoding: :auto }
|
|
485
|
-
{ template_object: ActiveAdminImport::Model.new(attributes) }
|
|
486
|
-
end
|
|
487
|
-
it 'should import file' do
|
|
488
|
-
with_zipped_csv(:authors_win1251_win_endline) do
|
|
489
|
-
upload_file!(:authors_win1251_win_endline, :zip)
|
|
490
|
-
expect(page).to have_content 'Successfully imported 2 authors'
|
|
491
|
-
expect(Author.count).to eq(2)
|
|
492
|
-
end
|
|
493
|
-
end
|
|
494
|
-
end
|
|
495
|
-
end
|
|
496
|
-
|
|
497
|
-
context 'with different header attribute names' do
|
|
498
|
-
let(:options) do
|
|
499
|
-
{ headers_rewrites: { :'Second name' => :last_name } }
|
|
500
|
-
end
|
|
501
|
-
|
|
502
|
-
it 'should import file' do
|
|
503
|
-
upload_file!(:author_broken_header)
|
|
504
|
-
expect(page).to have_content 'Successfully imported 1 author'
|
|
505
|
-
expect(Author.count).to eq(1)
|
|
506
|
-
end
|
|
507
|
-
end
|
|
508
|
-
|
|
509
|
-
context 'with semicolons separator' do
|
|
510
|
-
let(:options) do
|
|
511
|
-
attributes = { csv_options: { col_sep: ';' } }
|
|
512
|
-
{ template_object: ActiveAdminImport::Model.new(attributes) }
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
it 'should import file' do
|
|
516
|
-
upload_file!(:authors_with_semicolons)
|
|
517
|
-
expect(page).to have_content 'Successfully imported 2 authors'
|
|
518
|
-
expect(Author.count).to eq(2)
|
|
519
|
-
end
|
|
520
|
-
end
|
|
521
|
-
|
|
522
|
-
context 'with tab separator' do
|
|
523
|
-
let(:options) do
|
|
524
|
-
attributes = { csv_options: { col_sep: "\t" } }
|
|
525
|
-
{ template_object: ActiveAdminImport::Model.new(attributes) }
|
|
526
|
-
end
|
|
527
|
-
|
|
528
|
-
it 'should import file' do
|
|
529
|
-
upload_file!(:authors_with_tabs, 'tsv')
|
|
530
|
-
expect(page).to have_content 'Successfully imported 2 authors'
|
|
531
|
-
expect(Author.count).to eq(2)
|
|
532
|
-
end
|
|
533
|
-
end
|
|
534
|
-
|
|
535
|
-
context 'with empty csv and auto detect encoding' do
|
|
536
|
-
let(:options) do
|
|
537
|
-
attributes = { force_encoding: :auto }
|
|
538
|
-
{ template_object: ActiveAdminImport::Model.new(attributes) }
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
before do
|
|
542
|
-
upload_file!(:empty)
|
|
543
|
-
end
|
|
544
|
-
|
|
545
|
-
it 'should render warning' do
|
|
546
|
-
expect(page).to have_content I18n.t('active_admin_import.file_empty_error')
|
|
547
|
-
expect(Author.count).to eq(0)
|
|
548
|
-
end
|
|
549
|
-
end
|
|
550
|
-
|
|
551
|
-
context 'with csv which has exceeded values' do
|
|
552
|
-
before do
|
|
553
|
-
upload_file!(:authors_values_exceeded_headers)
|
|
554
|
-
end
|
|
555
|
-
|
|
556
|
-
it 'should render warning' do
|
|
557
|
-
# 5 columns: 'birthday, name, last_name, created_at, updated_at'
|
|
558
|
-
# 6 values: '"1988-11-16", "Jane", "Roe", " exceeded value", datetime, datetime'
|
|
559
|
-
msg = 'Number of values (6) exceeds number of columns (5)'
|
|
560
|
-
expect(page).to have_content I18n.t('active_admin_import.file_error', message: msg)
|
|
561
|
-
expect(Author.count).to eq(0)
|
|
562
|
-
end
|
|
563
|
-
end
|
|
564
|
-
end
|
|
565
|
-
|
|
566
|
-
context 'with callback procs options' do
|
|
567
|
-
let(:options) do
|
|
568
|
-
{
|
|
569
|
-
before_import: ->(_) { true },
|
|
570
|
-
after_import: ->(_) { true },
|
|
571
|
-
before_batch_import: ->(_) { true },
|
|
572
|
-
after_batch_import: ->(_) { true }
|
|
573
|
-
}
|
|
574
|
-
end
|
|
575
|
-
|
|
576
|
-
it 'should call each callback' do
|
|
577
|
-
expect(options[:before_import]).to receive(:call).with(kind_of(ActiveAdminImport::Importer))
|
|
578
|
-
expect(options[:after_import]).to receive(:call).with(kind_of(ActiveAdminImport::Importer))
|
|
579
|
-
expect(options[:before_batch_import]).to receive(:call).with(kind_of(ActiveAdminImport::Importer))
|
|
580
|
-
expect(options[:after_batch_import]).to receive(:call).with(kind_of(ActiveAdminImport::Importer))
|
|
581
|
-
upload_file!(:authors)
|
|
582
|
-
expect(Author.count).to eq(2)
|
|
583
|
-
end
|
|
584
|
-
|
|
585
|
-
context 'when the option before_import raises a ActiveAdminImport::Exception' do
|
|
586
|
-
let(:options) { { before_import: ->(_) { raise ActiveAdminImport::Exception, 'error message' } } }
|
|
587
|
-
|
|
588
|
-
before { upload_file!(:authors) }
|
|
589
|
-
|
|
590
|
-
it 'should show error' do
|
|
591
|
-
expect(page).to have_content I18n.t('active_admin_import.file_error', message: 'error message')
|
|
592
|
-
expect(Author.count).to eq(0)
|
|
593
|
-
end
|
|
594
|
-
end
|
|
595
|
-
|
|
596
|
-
context 'when the option before_batch_import raises a ActiveAdminImport::Exception' do
|
|
597
|
-
let(:options) { { before_batch_import: ->(_) { raise ActiveAdminImport::Exception, 'error message' } } }
|
|
598
|
-
|
|
599
|
-
before { upload_file!(:authors) }
|
|
600
|
-
|
|
601
|
-
it 'should show error' do
|
|
602
|
-
expect(page).to have_content I18n.t('active_admin_import.file_error', message: 'error message')
|
|
603
|
-
expect(Author.count).to eq(0)
|
|
604
|
-
end
|
|
605
|
-
end
|
|
606
|
-
end
|
|
607
|
-
end
|
|
608
|
-
|
|
609
|
-
context "with slice_columns option" do
|
|
610
|
-
let(:batch_size) { 2 }
|
|
611
|
-
|
|
612
|
-
before do
|
|
613
|
-
add_author_resource template_object: ActiveAdminImport::Model.new,
|
|
614
|
-
before_batch_import: lambda { |importer|
|
|
615
|
-
importer.batch_slice_columns(slice_columns)
|
|
616
|
-
},
|
|
617
|
-
batch_size: batch_size
|
|
618
|
-
visit "/admin/authors/import"
|
|
619
|
-
upload_file!(:authors)
|
|
620
|
-
end
|
|
621
|
-
|
|
622
|
-
context "slice last column and superfluous column" do
|
|
623
|
-
let(:slice_columns) { %w(name last_name not_existing_column) }
|
|
624
|
-
|
|
625
|
-
shared_examples_for "birthday column removed" do
|
|
626
|
-
it "should not fill `birthday` column" do
|
|
627
|
-
expect(Author.pluck(:name, :last_name, :birthday)).to match_array(
|
|
628
|
-
[
|
|
629
|
-
["Jane", "Roe", nil],
|
|
630
|
-
["John", "Doe", nil]
|
|
631
|
-
]
|
|
632
|
-
)
|
|
633
|
-
end
|
|
634
|
-
end
|
|
635
|
-
|
|
636
|
-
it_behaves_like "birthday column removed"
|
|
637
|
-
|
|
638
|
-
context "when doing more than one batch" do
|
|
639
|
-
let(:batch_size) { 1 }
|
|
640
|
-
|
|
641
|
-
it_behaves_like "birthday column removed"
|
|
642
|
-
end
|
|
643
|
-
end
|
|
644
|
-
|
|
645
|
-
context "slice column from the middle" do
|
|
646
|
-
let(:slice_columns) { %w(name birthday) }
|
|
647
|
-
|
|
648
|
-
it "should not fill `last_name` column" do
|
|
649
|
-
expect(Author.pluck(:name, :last_name, :birthday)).to match_array(
|
|
650
|
-
[
|
|
651
|
-
["Jane", nil, "1988-11-16".to_date],
|
|
652
|
-
["John", nil, "1986-05-01".to_date]
|
|
653
|
-
]
|
|
654
|
-
)
|
|
655
|
-
end
|
|
656
|
-
end
|
|
657
|
-
end
|
|
658
|
-
|
|
659
|
-
# Issue #186: each call must slice the result of the previous one, not re-apply
|
|
660
|
-
# the first call's indices to an already-sliced row.
|
|
661
|
-
context "with successive batch_slice_columns calls" do
|
|
662
|
-
before do
|
|
663
|
-
# validate: false isolates the slicing behaviour from the model's
|
|
664
|
-
# uniqueness validation, which would otherwise reject a later batch's
|
|
665
|
-
# author for sharing a NULL last_name with an earlier one.
|
|
666
|
-
add_author_resource template_object: ActiveAdminImport::Model.new,
|
|
667
|
-
validate: false,
|
|
668
|
-
before_batch_import: lambda { |importer|
|
|
669
|
-
importer.batch_slice_columns(%w(name last_name))
|
|
670
|
-
importer.batch_slice_columns(%w(name))
|
|
671
|
-
},
|
|
672
|
-
batch_size: batch_size
|
|
673
|
-
visit "/admin/authors/import"
|
|
674
|
-
upload_file!(fixture)
|
|
675
|
-
end
|
|
676
|
-
|
|
677
|
-
context "within a single batch" do
|
|
678
|
-
let(:batch_size) { 2 } # authors.csv has 2 rows -> 1 batch
|
|
679
|
-
let(:fixture) { :authors }
|
|
680
|
-
|
|
681
|
-
it "narrows the columns progressively" do
|
|
682
|
-
expect(Author.pluck(:name, :last_name, :birthday)).to match_array(
|
|
683
|
-
[
|
|
684
|
-
["Jane", nil, nil],
|
|
685
|
-
["John", nil, nil]
|
|
686
|
-
]
|
|
687
|
-
)
|
|
688
|
-
end
|
|
689
|
-
end
|
|
690
|
-
|
|
691
|
-
context "across several batches" do
|
|
692
|
-
let(:batch_size) { 2 } # authors_many.csv has 5 rows -> 3 batches
|
|
693
|
-
let(:fixture) { :authors_many }
|
|
694
|
-
|
|
695
|
-
it "narrows the columns progressively in every batch" do
|
|
696
|
-
expect(Author.pluck(:name, :last_name, :birthday)).to match_array(
|
|
697
|
-
[
|
|
698
|
-
["John", nil, nil],
|
|
699
|
-
["Jane", nil, nil],
|
|
700
|
-
["Jack", nil, nil],
|
|
701
|
-
["Jill", nil, nil],
|
|
702
|
-
["Joe", nil, nil]
|
|
703
|
-
]
|
|
704
|
-
)
|
|
705
|
-
end
|
|
706
|
-
end
|
|
707
|
-
end
|
|
708
|
-
|
|
709
|
-
context 'with invalid options' do
|
|
710
|
-
let(:options) { { invalid_option: :invalid_value } }
|
|
711
|
-
|
|
712
|
-
it 'should raise TypeError' do
|
|
713
|
-
expect { add_author_resource(options) }.to raise_error(ArgumentError)
|
|
714
|
-
end
|
|
715
|
-
end
|
|
716
|
-
|
|
717
|
-
context 'when submitting empty form after validation error' do
|
|
718
|
-
let(:options) { {} }
|
|
719
|
-
|
|
720
|
-
before do
|
|
721
|
-
add_author_resource(options)
|
|
722
|
-
visit '/admin/authors/import'
|
|
723
|
-
end
|
|
724
|
-
|
|
725
|
-
it 'should NOT reuse cached file from previous submission' do
|
|
726
|
-
expect do
|
|
727
|
-
upload_file!(:author_broken_header)
|
|
728
|
-
expect(page).to have_content("can't write unknown attribute")
|
|
729
|
-
end.not_to change { Author.count }
|
|
730
|
-
|
|
731
|
-
# Second submission without selecting a file
|
|
732
|
-
expect do
|
|
733
|
-
find_button(ImportFormSelectors.import_button_text).click
|
|
734
|
-
expect(page).to have_content(I18n.t('active_admin_import.no_file_error'))
|
|
735
|
-
end.not_to change { Author.count }
|
|
736
|
-
end
|
|
737
|
-
end
|
|
738
|
-
|
|
739
|
-
context 'when importing file with invalid format and auto force_encoding' do
|
|
740
|
-
let(:options) { { template_object: ActiveAdminImport::Model.new(force_encoding: :auto) } }
|
|
741
|
-
|
|
742
|
-
before do
|
|
743
|
-
add_author_resource(options)
|
|
744
|
-
visit '/admin/authors/import'
|
|
745
|
-
end
|
|
746
|
-
|
|
747
|
-
it 'should reject invalid file format before encoding' do
|
|
748
|
-
expect do
|
|
749
|
-
upload_file!(:author_invalid_format, 'txt')
|
|
750
|
-
expect(page).to have_content I18n.t('active_admin_import.file_format_error')
|
|
751
|
-
end.not_to change { Author.count }
|
|
752
|
-
end
|
|
753
|
-
end
|
|
754
|
-
|
|
755
|
-
context 'with active_admin_import_context defined on the controller' do
|
|
756
|
-
before { Author.create!(name: 'John', last_name: 'Doe') }
|
|
757
|
-
|
|
758
|
-
let(:author) { Author.take }
|
|
759
|
-
|
|
760
|
-
context 'when context returns request-derived attributes' do
|
|
761
|
-
before do
|
|
762
|
-
author_id = author.id
|
|
763
|
-
add_post_resource(
|
|
764
|
-
template_object: ActiveAdminImport::Model.new(author_id: author_id),
|
|
765
|
-
before_batch_import: lambda do |importer|
|
|
766
|
-
ip = importer.model.request_ip
|
|
767
|
-
a_id = importer.model.author_id
|
|
768
|
-
importer.csv_lines.map! { |row| row << ip << a_id }
|
|
769
|
-
importer.headers.merge!(:'Request Ip' => :request_ip, :'Author Id' => :author_id)
|
|
770
|
-
end,
|
|
771
|
-
controller_block: proc do
|
|
772
|
-
def active_admin_import_context
|
|
773
|
-
{ request_ip: request.remote_ip }
|
|
774
|
-
end
|
|
775
|
-
end
|
|
776
|
-
)
|
|
777
|
-
visit '/admin/posts/import'
|
|
778
|
-
upload_file!(:posts_for_author)
|
|
779
|
-
end
|
|
780
|
-
|
|
781
|
-
it 'merges the context into the import model so callbacks see it' do
|
|
782
|
-
expect(page).to have_content 'Successfully imported 2 posts'
|
|
783
|
-
expect(Post.count).to eq(2)
|
|
784
|
-
Post.all.each do |post|
|
|
785
|
-
expect(post.request_ip).to eq('127.0.0.1')
|
|
786
|
-
expect(post.author_id).to eq(author.id)
|
|
787
|
-
end
|
|
788
|
-
end
|
|
789
|
-
end
|
|
790
|
-
|
|
791
|
-
context 'when context returns parent id for a nested belongs_to resource' do
|
|
792
|
-
let(:post) { Post.create!(title: 'A post', body: 'body', author: author) }
|
|
793
|
-
|
|
794
|
-
before do
|
|
795
|
-
add_nested_post_comment_resource(
|
|
796
|
-
before_batch_import: lambda do |importer|
|
|
797
|
-
importer.csv_lines.map! { |row| row << importer.model.post_id }
|
|
798
|
-
importer.headers.merge!(:'Post Id' => :post_id)
|
|
799
|
-
end,
|
|
800
|
-
controller_block: proc do
|
|
801
|
-
def active_admin_import_context
|
|
802
|
-
{ post_id: parent.id }
|
|
803
|
-
end
|
|
804
|
-
end
|
|
805
|
-
)
|
|
806
|
-
visit "/admin/posts/#{post.id}/post_comments/import"
|
|
807
|
-
upload_file!(:post_comments)
|
|
808
|
-
end
|
|
809
|
-
|
|
810
|
-
it 'automatically assigns the parent post_id to every imported comment' do
|
|
811
|
-
expect(page).to have_content 'Successfully imported 2 post comments'
|
|
812
|
-
expect(PostComment.count).to eq(2)
|
|
813
|
-
expect(PostComment.where(post_id: post.id).count).to eq(2)
|
|
814
|
-
end
|
|
815
|
-
end
|
|
816
|
-
end
|
|
817
|
-
|
|
818
|
-
# PG-only: activerecord-import populates `result.ids` reliably on PostgreSQL
|
|
819
|
-
# via RETURNING. On MySQL/SQLite the array is not populated by default, so
|
|
820
|
-
# the assertion would not be meaningful there. The :result_class option
|
|
821
|
-
# itself works on every adapter — this spec just demonstrates the canonical
|
|
822
|
-
# PR #191 use case (collecting inserted ids) on the adapter that supports it.
|
|
823
|
-
if ENV['DB'] == 'postgres'
|
|
824
|
-
context 'with custom result_class (PostgreSQL)' do
|
|
825
|
-
# Subclass that captures inserted ids alongside the standard counters.
|
|
826
|
-
# Lives in user-land so the gem itself stays free of adapter-specific
|
|
827
|
-
# quirks around RETURNING. This is the example documented in the README.
|
|
828
|
-
class ImportResultWithIds < ActiveAdminImport::ImportResult
|
|
829
|
-
attr_reader :ids
|
|
830
|
-
|
|
831
|
-
def initialize
|
|
832
|
-
super
|
|
833
|
-
@ids = []
|
|
834
|
-
end
|
|
835
|
-
|
|
836
|
-
def add(batch_result, qty)
|
|
837
|
-
super
|
|
838
|
-
@ids.concat(Array(batch_result.ids))
|
|
839
|
-
end
|
|
840
|
-
end
|
|
841
|
-
|
|
842
|
-
before do
|
|
843
|
-
add_author_resource(result_class: ImportResultWithIds) do |result, _options|
|
|
844
|
-
# Expose the captured ids on the flash so the test asserts via the
|
|
845
|
-
# rendered page rather than closure capture.
|
|
846
|
-
flash[:notice] = "Imported ids: [#{result.ids.sort.join(',')}]"
|
|
847
|
-
end
|
|
848
|
-
visit '/admin/authors/import'
|
|
849
|
-
upload_file!(:authors)
|
|
850
|
-
end
|
|
851
|
-
|
|
852
|
-
it 'collects the ids of inserted records via the custom subclass' do
|
|
853
|
-
expect(Author.count).to eq(2)
|
|
854
|
-
expected = "Imported ids: [#{Author.pluck(:id).sort.join(',')}]"
|
|
855
|
-
expect(page).to have_content(expected)
|
|
856
|
-
end
|
|
857
|
-
end
|
|
858
|
-
end
|
|
859
|
-
end
|