active_admin_import 6.0.0 → 8.0.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bdc784c6f8a845b491b0dc93e44b073b548bccc182bb3cf6c26cb9f80e59b587
4
- data.tar.gz: 798237101d7d4403ed8fb1d1d4ed466b4b78bc44740e20137887d2c8b6464e3f
3
+ metadata.gz: f6e8f38e5549666b0bc1cd0d996f53fc0bebdfa5449750434fa4dbeb2ec4bc3b
4
+ data.tar.gz: 47298769ad0b0c7dfae9b21b470cf78b1164e6978aeb374ee9d4991de037814e
5
5
  SHA512:
6
- metadata.gz: '06639b5514e0c5af1f2cfd70f66f7de10b798fc22f9153b4870c0859b3f95fd372cf73f406b15b7df8b3e4d6fc350a911680248e09d15412479c55f092869a0d'
7
- data.tar.gz: 1b49c36c911a6eae50d653eafa4063a139b5d901b81264b213ece4bb995158bf7685f62facce2d5d1a69b76fe2735483296a26f9f697f6fad802d562ca3deef5
6
+ metadata.gz: b45454513b6a1312b1dd0490ab0f34def99e0928177a3fc209220be9fcb7d6cb371b0c9d086818ef8318f62d97ef39f8f38264acdef32d5795af02c90d54332c
7
+ data.tar.gz: c8a7a5aa657e2e78f9678e5e4da6ab348af718473b5431bdbbd84393050b58da560970da80b4d81904e901ddc13e7ca1f98b63e5bcb2367c41c8f2aa023cc7d3
@@ -16,12 +16,27 @@ jobs:
16
16
  strategy:
17
17
  fail-fast: false
18
18
  matrix:
19
- ruby: ['3.2', '3.3', '3.4']
20
- rails: ['7.1.0', '7.2.0', '8.0.0']
19
+ ruby: ['3.3', '3.4', '4.0']
20
+ rails: ['7.2.0', '8.0.0', '8.1.0']
21
21
  activeadmin: ['3.2.0', '3.3.0', '3.4.0', '3.5.1']
22
22
  exclude:
23
+ # ActiveAdmin 3.2.0 predates Rails 8 support.
23
24
  - rails: '8.0.0'
24
25
  activeadmin: '3.2.0'
26
+ - rails: '8.1.0'
27
+ activeadmin: '3.2.0'
28
+ include:
29
+ # ActiveAdmin 4 requires railties >= 7.2, so cover its floor and the
30
+ # newest supported Rails releases.
31
+ - ruby: '3.4'
32
+ rails: '7.2.0'
33
+ activeadmin: '4.0.0.beta22'
34
+ - ruby: '3.4'
35
+ rails: '8.0.0'
36
+ activeadmin: '4.0.0.beta22'
37
+ - ruby: '3.4'
38
+ rails: '8.1.0'
39
+ activeadmin: '4.0.0.beta22'
25
40
  env:
26
41
  RAILS: ${{ matrix.rails }}
27
42
  AA: ${{ matrix.activeadmin }}
@@ -31,6 +46,11 @@ jobs:
31
46
  with:
32
47
  ruby-version: ${{ matrix.ruby }}
33
48
  bundler-cache: true
49
+ - name: AA v4 environment summary
50
+ if: matrix.activeadmin == '4.0.0.beta22'
51
+ run: |
52
+ bundle exec gem list activeadmin
53
+ bundle info activeadmin
34
54
  - name: Run tests
35
55
  run: bundle exec rspec spec
36
56
  test-mysql:
@@ -98,6 +118,43 @@ jobs:
98
118
  bundler-cache: true
99
119
  - name: Run tests
100
120
  run: bundle exec rspec spec
121
+ test-postgres-aa4:
122
+ name: Ruby 3.4 / Rails 8.0.0 / AA 4.0.0.beta22 / PostgreSQL 16
123
+ runs-on: ubuntu-latest
124
+ env:
125
+ RAILS: '8.0.0'
126
+ AA: '4.0.0.beta22'
127
+ DB: postgres
128
+ DB_HOST: 127.0.0.1
129
+ DB_PORT: 5432
130
+ DB_USERNAME: postgres
131
+ DB_PASSWORD: postgres
132
+ services:
133
+ postgres:
134
+ image: postgres:16
135
+ env:
136
+ POSTGRES_USER: postgres
137
+ POSTGRES_PASSWORD: postgres
138
+ POSTGRES_DB: active_admin_import_test
139
+ ports:
140
+ - 5432:5432
141
+ options: >-
142
+ --health-cmd="pg_isready -U postgres"
143
+ --health-interval=10s
144
+ --health-timeout=5s
145
+ --health-retries=10
146
+ steps:
147
+ - uses: actions/checkout@v4
148
+ - uses: ruby/setup-ruby@v1
149
+ with:
150
+ ruby-version: '3.4'
151
+ bundler-cache: true
152
+ - name: AA v4 environment summary
153
+ run: |
154
+ bundle exec gem list activeadmin
155
+ bundle info activeadmin
156
+ - name: Run tests
157
+ run: bundle exec rspec spec
101
158
  coverage:
102
159
  name: Coverage
103
160
  runs-on: ubuntu-latest
data/Gemfile CHANGED
@@ -1,13 +1,25 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
3
 
4
- default_rails_version = '7.1.0'
5
- default_activeadmin_version = '3.2.0'
4
+ default_rails_version = '8.0.0'
5
+ default_activeadmin_version = '3.5.1'
6
+
7
+ # `~> 4.0.0.beta22` would admit 4.0.0 GA — pin prereleases exactly so the
8
+ # CI cell tests the AA build it claims to test.
9
+ aa_version = ENV['AA'] || default_activeadmin_version
10
+ aa_op = aa_version.match?(/[a-z]/) ? '=' : '~>'
6
11
 
7
12
  gem 'rails', "~> #{ENV['RAILS'] || default_rails_version}"
8
- gem 'activeadmin', "~> #{ENV['AA'] || default_activeadmin_version}"
9
- gem 'sprockets-rails'
10
- gem 'sass-rails'
13
+ gem 'activeadmin', "#{aa_op} #{aa_version}"
14
+
15
+ if ENV['AA']&.start_with?('4')
16
+ # AA 4 uses Tailwind + importmap; sass-rails conflicts with Tailwind v4.
17
+ gem 'cssbundling-rails'
18
+ gem 'importmap-rails'
19
+ else
20
+ gem 'sprockets-rails'
21
+ gem 'sass-rails'
22
+ end
11
23
 
12
24
  group :test do
13
25
  gem 'simplecov', require: false
@@ -22,6 +34,4 @@ group :test do
22
34
  end
23
35
  gem 'database_cleaner'
24
36
  gem 'capybara'
25
- gem 'cuprite'
26
- gem 'webrick', require: false
27
37
  end
data/README.md CHANGED
@@ -73,6 +73,7 @@ Tool | Description
73
73
  :error_limit |Limit the number of errors reported (default `5`, set to `nil` for all)
74
74
  :headers_rewrites |hash with key (csv header) - value (db column name) rows mapping
75
75
  :if |Controls whether the 'Import' button is displayed. It supports a proc to be evaluated into a boolean value within the activeadmin render context.
76
+ :action_item_html_options |HTML options passed to the index-page "Import …" action_item link. Defaults to `{ class: 'action-item-button' }` so the link matches AA 4's built-in action_items; the class is a no-op on AA 3. Override to drop the class or add your own (`{ class: 'my-btn' }`, `{ class: '', data: { turbo: false } }`, etc.).
76
77
 
77
78
 
78
79
 
@@ -224,7 +225,43 @@ end
224
225
 
225
226
  ##### Update existing records by id
226
227
 
227
- Delete colliding rows just before each batch insert:
228
+ Two strategies, depending on your database and whether you need validations.
229
+
230
+ ###### Native upsert (recommended where supported)
231
+
232
+ On databases that support upserts (MySQL, PostgreSQL 9.5+, SQLite 3.24+),
233
+ `:on_duplicate_key_update` updates colliding rows and inserts new ones in a
234
+ single statement — no extra delete. The option is passed straight to
235
+ `activerecord-import`, so its shape depends on the adapter:
236
+
237
+ ```ruby
238
+ # PostgreSQL / SQLite
239
+ on_duplicate_key_update: { conflict_target: [:id], columns: %i[name last_name birthday] }
240
+
241
+ # MySQL (infers the key from the columns)
242
+ on_duplicate_key_update: %i[name last_name birthday]
243
+ ```
244
+
245
+ ```ruby
246
+ ActiveAdmin.register Author do
247
+ active_admin_import validate: false,
248
+ on_duplicate_key_update: { conflict_target: [:id], columns: %i[name last_name birthday] }
249
+ end
250
+ ```
251
+
252
+ Notes:
253
+
254
+ * Only the columns you list are updated; other columns on the existing row keep their values.
255
+ * Use `validate: false` — `activerecord-import` runs uniqueness validations against the very rows the upsert is about to overwrite, so `validates_uniqueness_of` would otherwise reject the update.
256
+ * Active Record callbacks are not fired for bulk imports.
257
+
258
+ ###### Delete-then-insert (any database)
259
+
260
+ When you can't rely on upsert support — an older database, or you need your model
261
+ validations to run — delete the colliding rows just before each batch insert.
262
+ The old row is gone before the insert, so `validates_uniqueness_of` doesn't trip,
263
+ at the cost of a second query and full-row **replacement** (columns absent from
264
+ the CSV are reset, not preserved):
228
265
 
229
266
  ```ruby
230
267
  ActiveAdmin.register Post do
@@ -234,8 +271,6 @@ ActiveAdmin.register Post do
234
271
  end
235
272
  ```
236
273
 
237
- For databases that support upserts you can use `:on_duplicate_key_update` instead.
238
-
239
274
  ##### Tune batch size
240
275
 
241
276
  ```ruby
@@ -9,7 +9,7 @@ Gem::Specification.new do |gem|
9
9
  gem.summary = 'ActiveAdmin import based on activerecord-import gem.'
10
10
  gem.homepage = 'https://github.com/activeadmin-plugins/active_admin_import'
11
11
  gem.license = 'MIT'
12
- gem.required_ruby_version = '>= 3.1.0'
12
+ gem.required_ruby_version = '>= 3.3.0'
13
13
  gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
14
14
  gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
15
15
  gem.name = 'active_admin_import'
@@ -18,5 +18,5 @@ Gem::Specification.new do |gem|
18
18
  gem.add_runtime_dependency 'activerecord-import', '>= 2.0'
19
19
  gem.add_runtime_dependency 'rchardet', '>= 1.6'
20
20
  gem.add_runtime_dependency 'rubyzip', '>= 1.2'
21
- gem.add_dependency 'activeadmin', '>= 3.0', '< 4.0'
21
+ gem.add_dependency 'activeadmin', '>= 3.0', '< 4.1'
22
22
  end
@@ -27,6 +27,8 @@ module ActiveAdminImport
27
27
  module DSL
28
28
  CONTEXT_METHOD = :active_admin_import_context
29
29
 
30
+ ACTIVE_ADMIN_V4 = Gem::Version.new(ActiveAdmin::VERSION) >= Gem::Version.new('4.0.0.beta1')
31
+
30
32
  def self.prepare_import_model(template_object, controller, params: nil)
31
33
  model = template_object.is_a?(Proc) ? template_object.call : template_object
32
34
  if params
@@ -45,7 +47,7 @@ module ActiveAdminImport
45
47
  model_name = options[:resource_label].downcase
46
48
  plural_model_name = options[:plural_resource_label].downcase
47
49
  if result.empty?
48
- flash[:warning] = I18n.t('active_admin_import.file_empty_error')
50
+ flash[ACTIVE_ADMIN_V4 ? :alert : :warning] = I18n.t('active_admin_import.file_empty_error')
49
51
  else
50
52
  if result.failed?
51
53
  flash[:error] = I18n.t(
@@ -81,7 +83,8 @@ module ActiveAdminImport
81
83
  if authorized?(ActiveAdminImport::Auth::IMPORT, active_admin_config.resource_class)
82
84
  link_to(
83
85
  I18n.t('active_admin_import.import_model', plural_model: options[:plural_resource_label]),
84
- action: :import
86
+ { action: :import },
87
+ options[:action_item_html_options]
85
88
  )
86
89
  end
87
90
  end
@@ -82,23 +82,15 @@ module ActiveAdminImport
82
82
  # end
83
83
  #
84
84
  def batch_slice_columns(slice_columns)
85
- # Only set @use_indexes for the first batch so that @use_indexes are in correct
86
- # position for subsequent batches
87
- unless defined?(@use_indexes)
88
- @use_indexes = []
89
- headers.values.each_with_index do |val, index|
90
- @use_indexes << index if val.in?(slice_columns)
91
- end
92
- return csv_lines if @use_indexes.empty?
93
-
94
- # slice CSV headers
95
- @headers = headers.to_a.values_at(*@use_indexes).to_h
96
- end
85
+ columns = headers.values
86
+ indexes = columns.each_index.select { |i| columns[i].in?(slice_columns) }
87
+ return csv_lines if indexes.empty?
97
88
 
98
- # slice CSV values
99
- csv_lines.map! do |line|
100
- line.values_at(*@use_indexes)
101
- end
89
+ # @headers is reset to the full set at the start of every batch (see
90
+ # #batch_import), so each call narrows the previous call's result and every
91
+ # batch slices the same way — calling this more than once now composes (#186).
92
+ @headers = headers.to_a.values_at(*indexes).to_h
93
+ csv_lines.map! { |line| line.values_at(*indexes) }
102
94
  end
103
95
 
104
96
  def values_at(header_key)
@@ -129,10 +121,18 @@ module ActiveAdminImport
129
121
  end
130
122
 
131
123
  def prepare_headers
132
- headers = self.headers.present? ? self.headers.map(&:to_s) : yield
133
- @headers = Hash[headers.zip(headers.map { |el| el.underscore.gsub(/\s+/, '_') })].with_indifferent_access
134
- @headers.merge!(options[:headers_rewrites].symbolize_keys.slice(*@headers.symbolize_keys.keys))
135
- @headers
124
+ names = self.headers.present? ? self.headers : yield
125
+ blank_positions = names.each_index.select { |i| names[i].to_s.strip.empty? }
126
+ unless blank_positions.empty?
127
+ raise ActiveAdminImport::Exception,
128
+ "blank column header at column #{blank_positions.map { |i| i + 1 }.join(', ')}"
129
+ end
130
+
131
+ names = names.map(&:to_s)
132
+ # @source_headers is the complete parsed header row; batch_import copies it
133
+ # into the per-batch working @headers (see #batch_import).
134
+ @source_headers = Hash[names.zip(names.map { |el| el.underscore.gsub(/\s+/, '_') })].with_indifferent_access
135
+ @source_headers.merge!(options[:headers_rewrites].symbolize_keys.slice(*@source_headers.symbolize_keys.keys))
136
136
  end
137
137
 
138
138
  def run_callback(name)
@@ -141,6 +141,9 @@ module ActiveAdminImport
141
141
 
142
142
  def batch_import
143
143
  batch_result = nil
144
+ # Every batch re-parses full-width rows, so restore the full header set
145
+ # before slicing; batch_slice_columns then narrows this working copy.
146
+ @headers = @source_headers.dup
144
147
  @resource.transaction do
145
148
  run_callback(:before_batch_import)
146
149
  batch_result = resource.import(headers.values, csv_lines, import_options)
@@ -22,7 +22,8 @@ module ActiveAdminImport
22
22
  :plural_resource_label,
23
23
  :error_limit,
24
24
  :headers_rewrites,
25
- :if
25
+ :if,
26
+ :action_item_html_options
26
27
  ].freeze
27
28
 
28
29
  def self.options_for(config, options = {})
@@ -39,7 +40,10 @@ module ActiveAdminImport
39
40
  plural_resource_label: config.plural_resource_label,
40
41
  error_limit: 5,
41
42
  headers_rewrites: {},
42
- if: true
43
+ if: true,
44
+ # AA 4's built-in action_items hardcode this class for Tailwind styling
45
+ # (lib/active_admin/resource/action_items.rb). It's a no-op on AA 3.
46
+ action_item_html_options: { class: 'action-item-button' }
43
47
  }.deep_merge(options)
44
48
  end
45
49
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveAdminImport
4
- VERSION = '6.0.0'
4
+ VERSION = '8.0.0'
5
5
  end
@@ -0,0 +1,3 @@
1
+ Name,Last name,Birthday,
2
+ John,Doe,1986-05-01,x
3
+ Jane,Roe,1988-11-16,y
@@ -0,0 +1,3 @@
1
+ Name,,Last name,Birthday
2
+ John,x,Doe,1986-05-01
3
+ Jane,y,Roe,1988-11-16
@@ -0,0 +1,3 @@
1
+ ,Name,Last name,Birthday
2
+ x,John,Doe,1986-05-01
3
+ y,Jane,Roe,1988-11-16
@@ -0,0 +1,6 @@
1
+ Birthday,Name,Last name
2
+ 1986-05-01,John,Doe
3
+ 1988-11-16,Jane,Roe
4
+ 1990-01-01,Jack,Smith
5
+ 1991-02-02,Jill,Jones
6
+ 1992-03-03,Joe,Brown
data/spec/import_spec.rb CHANGED
@@ -41,8 +41,8 @@ describe 'import', type: :feature do
41
41
  end
42
42
 
43
43
  def upload_file!(name, ext = 'csv')
44
- attach_file('active_admin_import_model_file', File.expand_path("./spec/fixtures/files/#{name}.#{ext}"))
45
- find_button('Import').click
44
+ attach_file(ImportFormSelectors.file_input_id, File.expand_path("./spec/fixtures/files/#{name}.#{ext}"))
45
+ find_button(ImportFormSelectors.import_button_text).click
46
46
  end
47
47
 
48
48
  context 'posts index' do
@@ -118,7 +118,7 @@ describe 'import', type: :feature do
118
118
  # reload page
119
119
  visit '/admin/posts/import'
120
120
  # submit form without file
121
- find_button('Import').click
121
+ find_button(ImportFormSelectors.import_button_text).click
122
122
  end
123
123
 
124
124
  it 'should render validation error' do
@@ -171,7 +171,7 @@ describe 'import', type: :feature do
171
171
  # TODO: removing this causes undefined method `ransack' for #<ActiveRecord::Relation []>
172
172
  allow_any_instance_of(Admin::AuthorsController).to receive(:find_collection).and_return(Author.all)
173
173
  visit '/admin/authors'
174
- find_link('Import Authors').click
174
+ find_link(ImportFormSelectors.import_link_text).click
175
175
  expect(current_path).to eq('/admin/authors/import')
176
176
  end
177
177
  end
@@ -190,6 +190,36 @@ describe 'import', type: :feature do
190
190
  end
191
191
  end
192
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
+
193
223
  context 'authors already exist' do
194
224
  before do
195
225
  Author.create!(id: 1, name: 'Jane', last_name: 'Roe')
@@ -217,6 +247,51 @@ describe 'import', type: :feature do
217
247
  expect(Author.find(2).name).to eq('Jane')
218
248
  end
219
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
220
295
  end
221
296
 
222
297
  context 'with valid options' do
@@ -228,14 +303,14 @@ describe 'import', type: :feature do
228
303
  end
229
304
 
230
305
  it 'has valid form' do
231
- form = find('#new_active_admin_import_model')
306
+ form = find(ImportFormSelectors.form_css)
232
307
  expect(form['action']).to eq('/admin/authors/do_import')
233
308
  expect(form['enctype']).to eq('multipart/form-data')
234
- file_input = form.find('input#active_admin_import_model_file')
309
+ file_input = form.find(ImportFormSelectors.file_input_css)
235
310
  expect(file_input[:type]).to eq('file')
236
311
  expect(file_input.value).to be_blank
237
- submit_input = form.find('#active_admin_import_model_submit_action input')
238
- expect(submit_input[:value]).to eq('Import')
312
+ submit_input = form.find(ImportFormSelectors.submit_css)
313
+ expect(submit_input[:value]).to eq(ImportFormSelectors.import_button_text)
239
314
  expect(submit_input[:type]).to eq('submit')
240
315
  end
241
316
 
@@ -261,7 +336,7 @@ describe 'import', type: :feature do
261
336
 
262
337
  context 'when no file' do
263
338
  it 'should render error' do
264
- find_button('Import').click
339
+ find_button(ImportFormSelectors.import_button_text).click
265
340
  expect(Author.count).to eq(0)
266
341
  expect(page).to have_content I18n.t('active_admin_import.no_file_error')
267
342
  end
@@ -581,6 +656,56 @@ describe 'import', type: :feature do
581
656
  end
582
657
  end
583
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
+
584
709
  context 'with invalid options' do
585
710
  let(:options) { { invalid_option: :invalid_value } }
586
711
 
@@ -605,7 +730,7 @@ describe 'import', type: :feature do
605
730
 
606
731
  # Second submission without selecting a file
607
732
  expect do
608
- find_button('Import').click
733
+ find_button(ImportFormSelectors.import_button_text).click
609
734
  expect(page).to have_content(I18n.t('active_admin_import.no_file_error'))
610
735
  end.not_to change { Author.count }
611
736
  end
data/spec/spec_helper.rb CHANGED
@@ -12,9 +12,9 @@ Bundler.setup
12
12
 
13
13
  ENV['RAILS_ENV'] = 'test'
14
14
  require 'rails'
15
+ require 'test_app_paths'
15
16
  ENV['RAILS'] = Rails.version
16
- ENV['DB'] ||= 'sqlite'
17
- ENV['RAILS_ROOT'] = File.expand_path("../rails/rails-#{ENV['RAILS']}-#{ENV['DB']}", __FILE__)
17
+ ENV['RAILS_ROOT'] = TestAppPaths.app_root
18
18
  system 'rake setup' unless File.exist?(ENV['RAILS_ROOT'])
19
19
 
20
20
  require 'active_model'
@@ -28,16 +28,13 @@ ActiveAdmin.application.current_user_method = false
28
28
 
29
29
  require 'rspec/rails'
30
30
  require 'support/admin'
31
+ require 'support/import_form_selectors'
31
32
  require 'capybara/rails'
32
33
  require 'capybara/rspec'
33
- require 'capybara/cuprite'
34
34
 
35
- Capybara.server = :webrick
36
- Capybara.register_driver :cuprite do |app|
37
- Capybara::Cuprite::Driver.new(app, headless: true, window_size: [1280, 800])
38
- end
39
- Capybara.javascript_driver = :cuprite
40
- Capybara.default_max_wait_time = 5
35
+ # Specs exercise ActiveAdmin through Capybara's default rack_test driver — no
36
+ # JavaScript or real browser is needed, so no Cuprite/Chrome or app server.
37
+ Capybara.default_driver = :rack_test
41
38
 
42
39
  RSpec.configure do |config|
43
40
  config.use_transactional_fixtures = false
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Formtastic 6 (AA 4) keeps the same DOM IDs as Formtastic 4 (AA 3) for this
4
+ # form, so one selector set serves both — branch here if a future AA shifts an ID.
5
+ module ImportFormSelectors
6
+ module_function
7
+
8
+ SELECTORS = {
9
+ form_id: 'new_active_admin_import_model',
10
+ file_input_id: 'active_admin_import_model_file',
11
+ file_input_css: 'input#active_admin_import_model_file',
12
+ submit_css: '#active_admin_import_model_submit_action input',
13
+ import_button_text: 'Import',
14
+ import_link_text: 'Import Authors'
15
+ }.freeze
16
+
17
+ def form_id = SELECTORS[:form_id]
18
+ def form_css = "##{form_id}"
19
+ def file_input_id = SELECTORS[:file_input_id]
20
+ def file_input_css = SELECTORS[:file_input_css]
21
+ def submit_css = SELECTORS[:submit_css]
22
+ def import_button_text = SELECTORS[:import_button_text]
23
+ def import_link_text = SELECTORS[:import_link_text]
24
+ end
@@ -50,8 +50,19 @@ gsub_file "config/environment.rb",
50
50
 
51
51
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
52
52
 
53
+ aa_v4 = ENV['AA']&.start_with?('4')
54
+
53
55
  generate :'active_admin:install --skip-users'
54
- generate :'formtastic:install'
56
+
57
+ if aa_v4
58
+ # `active_admin:assets` swaps AA 3's Sprockets SCSS/JS for AA 4's Tailwind CSS
59
+ # stub. We don't compile it — specs assert on DOM and flash text, not styling,
60
+ # so the stub suffices and no Node is needed. `builds/` satisfies cssbundling-rails.
61
+ generate :'active_admin:assets'
62
+ run 'mkdir -p app/assets/builds'
63
+ else
64
+ generate :'formtastic:install'
65
+ end
55
66
 
56
67
  run 'rm -rf test'
57
68
  route "root :to => 'admin/dashboard#index'"
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestAppPaths
4
+ module_function
5
+
6
+ def app_dir_name
7
+ "rails-#{Rails::VERSION::STRING}-#{ENV['DB'] || 'sqlite'}-aa#{ENV['AA'] || 'default'}"
8
+ end
9
+
10
+ # Absolute path under spec/rails/, used as RAILS_ROOT.
11
+ def app_root
12
+ File.expand_path("../rails/#{app_dir_name}", __dir__)
13
+ end
14
+ end
data/tasks/test.rake CHANGED
@@ -1,3 +1,5 @@
1
+ require_relative '../spec/support/test_app_paths'
2
+
1
3
  desc "Creates a test rails app for the specs to run against"
2
4
  task :setup do
3
5
  require 'rails/version'
@@ -8,6 +10,9 @@ task :setup do
8
10
  when 'postgres', 'postgresql' then 'postgresql'
9
11
  else 'sqlite3'
10
12
  end
13
+ aa_v4 = ENV['AA']&.start_with?('4')
14
+
15
+ puts "[setup] ActiveAdmin: #{ENV['AA'] || '(Gemfile default)'} / Rails: #{Rails::VERSION::STRING} / DB: #{rails_db}"
11
16
 
12
17
  rails_new_opts = %W(
13
18
  --skip-turbolinks
@@ -17,5 +22,18 @@ task :setup do
17
22
  -m
18
23
  spec/support/rails_template.rb
19
24
  )
20
- system "bundle exec rails new spec/rails/rails-#{Rails::VERSION::STRING}-#{db} #{rails_new_opts.join(' ')}"
25
+ # v4 drops sprockets-rails (see Gemfile), so skip the asset pipeline to
26
+ # avoid the auto-generated `config/initializers/assets.rb` crashing at boot.
27
+ if aa_v4
28
+ rails_new_opts.unshift('--skip-asset-pipeline')
29
+ else
30
+ # Rails 8.1 wires importmap-rails into the generated ApplicationController
31
+ # (`stale_when_importmap_changes`). The AA 3 app uses Sprockets, not
32
+ # importmap, so that gem is absent and the controller raises NameError at
33
+ # boot. Skip JavaScript so the macro is never generated — the specs run on
34
+ # rack_test and never execute JS anyway.
35
+ rails_new_opts.unshift('--skip-javascript')
36
+ end
37
+
38
+ system "bundle exec rails new spec/rails/#{TestAppPaths.app_dir_name} #{rails_new_opts.join(' ')}"
21
39
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_admin_import
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.0
4
+ version: 8.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Fedoronchuk
@@ -60,7 +60,7 @@ dependencies:
60
60
  version: '3.0'
61
61
  - - "<"
62
62
  - !ruby/object:Gem::Version
63
- version: '4.0'
63
+ version: '4.1'
64
64
  type: :runtime
65
65
  prerelease: false
66
66
  version_requirements: !ruby/object:Gem::Requirement
@@ -70,7 +70,7 @@ dependencies:
70
70
  version: '3.0'
71
71
  - - "<"
72
72
  - !ruby/object:Gem::Version
73
- version: '4.0'
73
+ version: '4.1'
74
74
  description: The most efficient way to import for Active Admin
75
75
  email:
76
76
  - fedoronchuk@gmail.com
@@ -114,9 +114,13 @@ files:
114
114
  - spec/fixtures/files/author_invalid.csv
115
115
  - spec/fixtures/files/author_invalid_format.txt
116
116
  - spec/fixtures/files/authors.csv
117
+ - spec/fixtures/files/authors_blank_header_end.csv
118
+ - spec/fixtures/files/authors_blank_header_middle.csv
117
119
  - spec/fixtures/files/authors_bom.csv
120
+ - spec/fixtures/files/authors_empty_header.csv
118
121
  - spec/fixtures/files/authors_invalid_db.csv
119
122
  - spec/fixtures/files/authors_invalid_model.csv
123
+ - spec/fixtures/files/authors_many.csv
120
124
  - spec/fixtures/files/authors_no_headers.csv
121
125
  - spec/fixtures/files/authors_values_exceeded_headers.csv
122
126
  - spec/fixtures/files/authors_win1251_win_endline.csv
@@ -135,7 +139,9 @@ files:
135
139
  - spec/spec_helper.rb
136
140
  - spec/support/active_model_lint.rb
137
141
  - spec/support/admin.rb
142
+ - spec/support/import_form_selectors.rb
138
143
  - spec/support/rails_template.rb
144
+ - spec/support/test_app_paths.rb
139
145
  - tasks/test.rake
140
146
  homepage: https://github.com/activeadmin-plugins/active_admin_import
141
147
  licenses:
@@ -148,7 +154,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
148
154
  requirements:
149
155
  - - ">="
150
156
  - !ruby/object:Gem::Version
151
- version: 3.1.0
157
+ version: 3.3.0
152
158
  required_rubygems_version: !ruby/object:Gem::Requirement
153
159
  requirements:
154
160
  - - ">="