active_admin_import 5.1.0 → 7.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 +4 -4
- data/.github/workflows/test.yml +199 -31
- data/Gemfile +28 -12
- data/README.md +253 -15
- data/Rakefile +1 -4
- data/active_admin_import.gemspec +4 -4
- data/lib/active_admin_import/dsl.rb +26 -15
- data/lib/active_admin_import/importer.rb +3 -2
- data/lib/active_admin_import/model.rb +2 -1
- data/lib/active_admin_import/options.rb +7 -2
- data/lib/active_admin_import/version.rb +1 -1
- data/spec/fixtures/files/author_invalid_format.txt +2 -0
- data/spec/fixtures/files/post_comments.csv +3 -0
- data/spec/import_spec.rb +153 -10
- data/spec/spec_helper.rb +11 -18
- data/spec/support/admin.rb +16 -0
- data/spec/support/import_form_selectors.rb +24 -0
- data/spec/support/rails_template.rb +58 -16
- data/spec/support/test_app_paths.rb +14 -0
- data/tasks/test.rake +27 -4
- metadata +20 -40
- data/CHANGELOG.md +0 -33
|
@@ -25,11 +25,29 @@ module ActiveAdminImport
|
|
|
25
25
|
# +plural_resource_label+:: pluralized resource label value (default config.plural_resource_label)
|
|
26
26
|
#
|
|
27
27
|
module DSL
|
|
28
|
+
CONTEXT_METHOD = :active_admin_import_context
|
|
29
|
+
|
|
30
|
+
ACTIVE_ADMIN_V4 = Gem::Version.new(ActiveAdmin::VERSION) >= Gem::Version.new('4.0.0.beta1')
|
|
31
|
+
|
|
32
|
+
def self.prepare_import_model(template_object, controller, params: nil)
|
|
33
|
+
model = template_object.is_a?(Proc) ? template_object.call : template_object
|
|
34
|
+
if params
|
|
35
|
+
params_key = ActiveModel::Naming.param_key(model.class)
|
|
36
|
+
model.assign_attributes(params[params_key].try(:deep_symbolize_keys) || {})
|
|
37
|
+
end
|
|
38
|
+
return model unless controller.respond_to?(CONTEXT_METHOD, true)
|
|
39
|
+
context = controller.send(CONTEXT_METHOD)
|
|
40
|
+
return model unless context.is_a?(Hash)
|
|
41
|
+
context = context.merge(file: model.file) if model.respond_to?(:file) && !context.key?(:file)
|
|
42
|
+
model.assign_attributes(context)
|
|
43
|
+
model
|
|
44
|
+
end
|
|
45
|
+
|
|
28
46
|
DEFAULT_RESULT_PROC = lambda do |result, options|
|
|
29
47
|
model_name = options[:resource_label].downcase
|
|
30
48
|
plural_model_name = options[:plural_resource_label].downcase
|
|
31
49
|
if result.empty?
|
|
32
|
-
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')
|
|
33
51
|
else
|
|
34
52
|
if result.failed?
|
|
35
53
|
flash[:error] = I18n.t(
|
|
@@ -57,11 +75,7 @@ module ActiveAdminImport
|
|
|
57
75
|
|
|
58
76
|
collection_action :import, method: :get do
|
|
59
77
|
authorize!(ActiveAdminImport::Auth::IMPORT, active_admin_config.resource_class)
|
|
60
|
-
@active_admin_import_model =
|
|
61
|
-
options[:template_object].call
|
|
62
|
-
else
|
|
63
|
-
options[:template_object]
|
|
64
|
-
end
|
|
78
|
+
@active_admin_import_model = ActiveAdminImport::DSL.prepare_import_model(options[:template_object], self)
|
|
65
79
|
render template: options[:template]
|
|
66
80
|
end
|
|
67
81
|
|
|
@@ -69,7 +83,8 @@ module ActiveAdminImport
|
|
|
69
83
|
if authorized?(ActiveAdminImport::Auth::IMPORT, active_admin_config.resource_class)
|
|
70
84
|
link_to(
|
|
71
85
|
I18n.t('active_admin_import.import_model', plural_model: options[:plural_resource_label]),
|
|
72
|
-
action: :import
|
|
86
|
+
{ action: :import },
|
|
87
|
+
options[:action_item_html_options]
|
|
73
88
|
)
|
|
74
89
|
end
|
|
75
90
|
end
|
|
@@ -78,13 +93,9 @@ module ActiveAdminImport
|
|
|
78
93
|
authorize!(ActiveAdminImport::Auth::IMPORT, active_admin_config.resource_class)
|
|
79
94
|
_params = params.respond_to?(:to_unsafe_h) ? params.to_unsafe_h : params
|
|
80
95
|
params = ActiveSupport::HashWithIndifferentAccess.new _params
|
|
81
|
-
@active_admin_import_model =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
options[:template_object]
|
|
85
|
-
end
|
|
86
|
-
params_key = ActiveModel::Naming.param_key(@active_admin_import_model.class)
|
|
87
|
-
@active_admin_import_model.assign_attributes(params[params_key].try(:deep_symbolize_keys) || {})
|
|
96
|
+
@active_admin_import_model = ActiveAdminImport::DSL.prepare_import_model(
|
|
97
|
+
options[:template_object], self, params: params
|
|
98
|
+
)
|
|
88
99
|
# go back to form
|
|
89
100
|
return render template: options[:template] unless @active_admin_import_model.valid?
|
|
90
101
|
@importer = Importer.new(
|
|
@@ -96,7 +107,7 @@ module ActiveAdminImport
|
|
|
96
107
|
result = @importer.import
|
|
97
108
|
|
|
98
109
|
if block_given?
|
|
99
|
-
|
|
110
|
+
instance_exec result, options, &block
|
|
100
111
|
else
|
|
101
112
|
instance_exec result, options, &DEFAULT_RESULT_PROC
|
|
102
113
|
end
|
|
@@ -18,7 +18,8 @@ module ActiveAdminImport
|
|
|
18
18
|
:headers_rewrites,
|
|
19
19
|
:batch_size,
|
|
20
20
|
:batch_transaction,
|
|
21
|
-
:csv_options
|
|
21
|
+
:csv_options,
|
|
22
|
+
:result_class
|
|
22
23
|
].freeze
|
|
23
24
|
|
|
24
25
|
def initialize(resource, model, options)
|
|
@@ -29,7 +30,7 @@ module ActiveAdminImport
|
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def import_result
|
|
32
|
-
@import_result ||= ImportResult.new
|
|
33
|
+
@import_result ||= (options[:result_class] || ImportResult).new
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def file
|
|
@@ -37,7 +37,7 @@ module ActiveAdminImport
|
|
|
37
37
|
validate :file_contents_present, if: ->(me) { me.file.present? }
|
|
38
38
|
|
|
39
39
|
before_validation :unzip_file, if: ->(me) { me.archive? && me.allow_archive? }
|
|
40
|
-
|
|
40
|
+
after_validation :encode_file, if: ->(me) { me.errors.empty? && me.force_encoding? && me.file.present? }
|
|
41
41
|
|
|
42
42
|
attr_reader :attributes
|
|
43
43
|
|
|
@@ -48,6 +48,7 @@ module ActiveAdminImport
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def assign_attributes(args = {}, new_record = false)
|
|
51
|
+
args[:file] = nil unless args.key?(:file)
|
|
51
52
|
@attributes.merge!(args)
|
|
52
53
|
@new_record = new_record
|
|
53
54
|
args.keys.each do |key|
|
|
@@ -16,12 +16,14 @@ module ActiveAdminImport
|
|
|
16
16
|
:ignore,
|
|
17
17
|
:template,
|
|
18
18
|
:template_object,
|
|
19
|
+
:result_class,
|
|
19
20
|
:resource_class,
|
|
20
21
|
:resource_label,
|
|
21
22
|
:plural_resource_label,
|
|
22
23
|
:error_limit,
|
|
23
24
|
:headers_rewrites,
|
|
24
|
-
:if
|
|
25
|
+
:if,
|
|
26
|
+
:action_item_html_options
|
|
25
27
|
].freeze
|
|
26
28
|
|
|
27
29
|
def self.options_for(config, options = {})
|
|
@@ -38,7 +40,10 @@ module ActiveAdminImport
|
|
|
38
40
|
plural_resource_label: config.plural_resource_label,
|
|
39
41
|
error_limit: 5,
|
|
40
42
|
headers_rewrites: {},
|
|
41
|
-
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' }
|
|
42
47
|
}.deep_merge(options)
|
|
43
48
|
end
|
|
44
49
|
end
|
data/spec/import_spec.rb
CHANGED
|
@@ -27,7 +27,7 @@ describe 'import', type: :feature do
|
|
|
27
27
|
zip_file = File.expand_path("./spec/fixtures/files/#{name}.zip")
|
|
28
28
|
|
|
29
29
|
begin
|
|
30
|
-
Zip::File.open(zip_file,
|
|
30
|
+
Zip::File.open(zip_file, create: true) do |z|
|
|
31
31
|
z.add "#{name}.csv", File.expand_path("./spec/fixtures/files/#{name}.csv")
|
|
32
32
|
end
|
|
33
33
|
instance_eval &block
|
|
@@ -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(
|
|
45
|
-
find_button(
|
|
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(
|
|
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(
|
|
174
|
+
find_link(ImportFormSelectors.import_link_text).click
|
|
175
175
|
expect(current_path).to eq('/admin/authors/import')
|
|
176
176
|
end
|
|
177
177
|
end
|
|
@@ -228,14 +228,14 @@ describe 'import', type: :feature do
|
|
|
228
228
|
end
|
|
229
229
|
|
|
230
230
|
it 'has valid form' do
|
|
231
|
-
form = find(
|
|
231
|
+
form = find(ImportFormSelectors.form_css)
|
|
232
232
|
expect(form['action']).to eq('/admin/authors/do_import')
|
|
233
233
|
expect(form['enctype']).to eq('multipart/form-data')
|
|
234
|
-
file_input = form.find(
|
|
234
|
+
file_input = form.find(ImportFormSelectors.file_input_css)
|
|
235
235
|
expect(file_input[:type]).to eq('file')
|
|
236
236
|
expect(file_input.value).to be_blank
|
|
237
|
-
submit_input = form.find(
|
|
238
|
-
expect(submit_input[:value]).to eq(
|
|
237
|
+
submit_input = form.find(ImportFormSelectors.submit_css)
|
|
238
|
+
expect(submit_input[:value]).to eq(ImportFormSelectors.import_button_text)
|
|
239
239
|
expect(submit_input[:type]).to eq('submit')
|
|
240
240
|
end
|
|
241
241
|
|
|
@@ -261,7 +261,7 @@ describe 'import', type: :feature do
|
|
|
261
261
|
|
|
262
262
|
context 'when no file' do
|
|
263
263
|
it 'should render error' do
|
|
264
|
-
find_button(
|
|
264
|
+
find_button(ImportFormSelectors.import_button_text).click
|
|
265
265
|
expect(Author.count).to eq(0)
|
|
266
266
|
expect(page).to have_content I18n.t('active_admin_import.no_file_error')
|
|
267
267
|
end
|
|
@@ -588,4 +588,147 @@ describe 'import', type: :feature do
|
|
|
588
588
|
expect { add_author_resource(options) }.to raise_error(ArgumentError)
|
|
589
589
|
end
|
|
590
590
|
end
|
|
591
|
+
|
|
592
|
+
context 'when submitting empty form after validation error' do
|
|
593
|
+
let(:options) { {} }
|
|
594
|
+
|
|
595
|
+
before do
|
|
596
|
+
add_author_resource(options)
|
|
597
|
+
visit '/admin/authors/import'
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
it 'should NOT reuse cached file from previous submission' do
|
|
601
|
+
expect do
|
|
602
|
+
upload_file!(:author_broken_header)
|
|
603
|
+
expect(page).to have_content("can't write unknown attribute")
|
|
604
|
+
end.not_to change { Author.count }
|
|
605
|
+
|
|
606
|
+
# Second submission without selecting a file
|
|
607
|
+
expect do
|
|
608
|
+
find_button(ImportFormSelectors.import_button_text).click
|
|
609
|
+
expect(page).to have_content(I18n.t('active_admin_import.no_file_error'))
|
|
610
|
+
end.not_to change { Author.count }
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
context 'when importing file with invalid format and auto force_encoding' do
|
|
615
|
+
let(:options) { { template_object: ActiveAdminImport::Model.new(force_encoding: :auto) } }
|
|
616
|
+
|
|
617
|
+
before do
|
|
618
|
+
add_author_resource(options)
|
|
619
|
+
visit '/admin/authors/import'
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
it 'should reject invalid file format before encoding' do
|
|
623
|
+
expect do
|
|
624
|
+
upload_file!(:author_invalid_format, 'txt')
|
|
625
|
+
expect(page).to have_content I18n.t('active_admin_import.file_format_error')
|
|
626
|
+
end.not_to change { Author.count }
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
context 'with active_admin_import_context defined on the controller' do
|
|
631
|
+
before { Author.create!(name: 'John', last_name: 'Doe') }
|
|
632
|
+
|
|
633
|
+
let(:author) { Author.take }
|
|
634
|
+
|
|
635
|
+
context 'when context returns request-derived attributes' do
|
|
636
|
+
before do
|
|
637
|
+
author_id = author.id
|
|
638
|
+
add_post_resource(
|
|
639
|
+
template_object: ActiveAdminImport::Model.new(author_id: author_id),
|
|
640
|
+
before_batch_import: lambda do |importer|
|
|
641
|
+
ip = importer.model.request_ip
|
|
642
|
+
a_id = importer.model.author_id
|
|
643
|
+
importer.csv_lines.map! { |row| row << ip << a_id }
|
|
644
|
+
importer.headers.merge!(:'Request Ip' => :request_ip, :'Author Id' => :author_id)
|
|
645
|
+
end,
|
|
646
|
+
controller_block: proc do
|
|
647
|
+
def active_admin_import_context
|
|
648
|
+
{ request_ip: request.remote_ip }
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
)
|
|
652
|
+
visit '/admin/posts/import'
|
|
653
|
+
upload_file!(:posts_for_author)
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
it 'merges the context into the import model so callbacks see it' do
|
|
657
|
+
expect(page).to have_content 'Successfully imported 2 posts'
|
|
658
|
+
expect(Post.count).to eq(2)
|
|
659
|
+
Post.all.each do |post|
|
|
660
|
+
expect(post.request_ip).to eq('127.0.0.1')
|
|
661
|
+
expect(post.author_id).to eq(author.id)
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
context 'when context returns parent id for a nested belongs_to resource' do
|
|
667
|
+
let(:post) { Post.create!(title: 'A post', body: 'body', author: author) }
|
|
668
|
+
|
|
669
|
+
before do
|
|
670
|
+
add_nested_post_comment_resource(
|
|
671
|
+
before_batch_import: lambda do |importer|
|
|
672
|
+
importer.csv_lines.map! { |row| row << importer.model.post_id }
|
|
673
|
+
importer.headers.merge!(:'Post Id' => :post_id)
|
|
674
|
+
end,
|
|
675
|
+
controller_block: proc do
|
|
676
|
+
def active_admin_import_context
|
|
677
|
+
{ post_id: parent.id }
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
)
|
|
681
|
+
visit "/admin/posts/#{post.id}/post_comments/import"
|
|
682
|
+
upload_file!(:post_comments)
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
it 'automatically assigns the parent post_id to every imported comment' do
|
|
686
|
+
expect(page).to have_content 'Successfully imported 2 post comments'
|
|
687
|
+
expect(PostComment.count).to eq(2)
|
|
688
|
+
expect(PostComment.where(post_id: post.id).count).to eq(2)
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
# PG-only: activerecord-import populates `result.ids` reliably on PostgreSQL
|
|
694
|
+
# via RETURNING. On MySQL/SQLite the array is not populated by default, so
|
|
695
|
+
# the assertion would not be meaningful there. The :result_class option
|
|
696
|
+
# itself works on every adapter — this spec just demonstrates the canonical
|
|
697
|
+
# PR #191 use case (collecting inserted ids) on the adapter that supports it.
|
|
698
|
+
if ENV['DB'] == 'postgres'
|
|
699
|
+
context 'with custom result_class (PostgreSQL)' do
|
|
700
|
+
# Subclass that captures inserted ids alongside the standard counters.
|
|
701
|
+
# Lives in user-land so the gem itself stays free of adapter-specific
|
|
702
|
+
# quirks around RETURNING. This is the example documented in the README.
|
|
703
|
+
class ImportResultWithIds < ActiveAdminImport::ImportResult
|
|
704
|
+
attr_reader :ids
|
|
705
|
+
|
|
706
|
+
def initialize
|
|
707
|
+
super
|
|
708
|
+
@ids = []
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
def add(batch_result, qty)
|
|
712
|
+
super
|
|
713
|
+
@ids.concat(Array(batch_result.ids))
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
before do
|
|
718
|
+
add_author_resource(result_class: ImportResultWithIds) do |result, _options|
|
|
719
|
+
# Expose the captured ids on the flash so the test asserts via the
|
|
720
|
+
# rendered page rather than closure capture.
|
|
721
|
+
flash[:notice] = "Imported ids: [#{result.ids.sort.join(',')}]"
|
|
722
|
+
end
|
|
723
|
+
visit '/admin/authors/import'
|
|
724
|
+
upload_file!(:authors)
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
it 'collects the ids of inserted records via the custom subclass' do
|
|
728
|
+
expect(Author.count).to eq(2)
|
|
729
|
+
expected = "Imported ids: [#{Author.pluck(:id).sort.join(',')}]"
|
|
730
|
+
expect(page).to have_content(expected)
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
end
|
|
591
734
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
require 'simplecov'
|
|
2
|
+
SimpleCov.start do
|
|
3
|
+
add_filter '/spec/'
|
|
4
|
+
end
|
|
4
5
|
|
|
5
6
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
6
7
|
$LOAD_PATH << File.expand_path('../support', __FILE__)
|
|
@@ -10,44 +11,36 @@ require 'bundler'
|
|
|
10
11
|
Bundler.setup
|
|
11
12
|
|
|
12
13
|
ENV['RAILS_ENV'] = 'test'
|
|
13
|
-
# Ensure the Active Admin load path is happy
|
|
14
14
|
require 'rails'
|
|
15
|
+
require 'test_app_paths'
|
|
15
16
|
ENV['RAILS'] = Rails.version
|
|
16
|
-
ENV['RAILS_ROOT'] =
|
|
17
|
-
# Create the test app if it doesn't exists
|
|
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'
|
|
21
|
-
# require ActiveRecord to ensure that Ransack loads correctly
|
|
22
21
|
require 'active_record'
|
|
23
22
|
require 'action_view'
|
|
24
23
|
require 'active_admin'
|
|
25
24
|
ActiveAdmin.application.load_paths = [ENV['RAILS_ROOT'] + '/app/admin']
|
|
26
25
|
require ENV['RAILS_ROOT'] + '/config/environment.rb'
|
|
27
|
-
# Disabling authentication in specs so that we don't have to worry about
|
|
28
|
-
# it allover the place
|
|
29
26
|
ActiveAdmin.application.authentication_method = false
|
|
30
27
|
ActiveAdmin.application.current_user_method = false
|
|
31
28
|
|
|
32
29
|
require 'rspec/rails'
|
|
33
30
|
require 'support/admin'
|
|
31
|
+
require 'support/import_form_selectors'
|
|
34
32
|
require 'capybara/rails'
|
|
35
33
|
require 'capybara/rspec'
|
|
36
|
-
require 'capybara/poltergeist'
|
|
37
|
-
|
|
38
|
-
Capybara.register_driver :poltergeist do |app|
|
|
39
|
-
Capybara::Poltergeist::Driver.new(app, js_errors: true,
|
|
40
|
-
timeout: 80,
|
|
41
|
-
debug: true,
|
|
42
|
-
phantomjs_options: ['--debug=no', '--load-images=no'])
|
|
43
|
-
end
|
|
44
34
|
|
|
45
|
-
Capybara
|
|
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
|
|
46
38
|
|
|
47
39
|
RSpec.configure do |config|
|
|
48
40
|
config.use_transactional_fixtures = false
|
|
49
41
|
|
|
50
42
|
config.before(:suite) do
|
|
43
|
+
ActiveRecord::Migration.maintain_test_schema!
|
|
51
44
|
DatabaseCleaner.strategy = :truncation
|
|
52
45
|
DatabaseCleaner.clean_with(:truncation)
|
|
53
46
|
end
|
data/spec/support/admin.rb
CHANGED
|
@@ -8,8 +8,24 @@ def add_author_resource(options = {}, &block)
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def add_post_resource(options = {}, &block)
|
|
11
|
+
cb = options.delete(:controller_block)
|
|
11
12
|
ActiveAdmin.register Post do
|
|
12
13
|
config.filters = false
|
|
14
|
+
controller(&cb) if cb
|
|
15
|
+
active_admin_import(options, &block)
|
|
16
|
+
end
|
|
17
|
+
Rails.application.reload_routes!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_nested_post_comment_resource(options = {}, &block)
|
|
21
|
+
cb = options.delete(:controller_block)
|
|
22
|
+
ActiveAdmin.register Post do
|
|
23
|
+
config.filters = false
|
|
24
|
+
end
|
|
25
|
+
ActiveAdmin.register PostComment do
|
|
26
|
+
config.filters = false
|
|
27
|
+
belongs_to :post
|
|
28
|
+
controller(&cb) if cb
|
|
13
29
|
active_admin_import(options, &block)
|
|
14
30
|
end
|
|
15
31
|
Rails.application.reload_routes!
|
|
@@ -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
|
|
@@ -1,29 +1,71 @@
|
|
|
1
|
-
|
|
2
|
-
# Rails template to build the sample app for specs
|
|
1
|
+
create_file "app/assets/config/manifest.js", skip: true
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
db = ENV['DB'] || 'sqlite'
|
|
4
|
+
case db
|
|
5
|
+
when 'mysql'
|
|
6
|
+
remove_file 'config/database.yml'
|
|
7
|
+
create_file 'config/database.yml', <<~YAML
|
|
8
|
+
default: &default
|
|
9
|
+
adapter: mysql2
|
|
10
|
+
encoding: utf8mb4
|
|
11
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
|
12
|
+
host: <%= ENV.fetch("DB_HOST", "127.0.0.1") %>
|
|
13
|
+
port: <%= ENV.fetch("DB_PORT", 3306) %>
|
|
14
|
+
username: <%= ENV.fetch("DB_USERNAME", "root") %>
|
|
15
|
+
password: <%= ENV.fetch("DB_PASSWORD", "root") %>
|
|
6
16
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
17
|
+
test:
|
|
18
|
+
<<: *default
|
|
19
|
+
database: active_admin_import_test
|
|
20
|
+
YAML
|
|
21
|
+
when 'postgres', 'postgresql'
|
|
22
|
+
remove_file 'config/database.yml'
|
|
23
|
+
create_file 'config/database.yml', <<~YAML
|
|
24
|
+
default: &default
|
|
25
|
+
adapter: postgresql
|
|
26
|
+
encoding: unicode
|
|
27
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
|
28
|
+
host: <%= ENV.fetch("DB_HOST", "127.0.0.1") %>
|
|
29
|
+
port: <%= ENV.fetch("DB_PORT", 5432) %>
|
|
30
|
+
username: <%= ENV.fetch("DB_USERNAME", "postgres") %>
|
|
31
|
+
password: <%= ENV.fetch("DB_PASSWORD", "postgres") %>
|
|
32
|
+
|
|
33
|
+
test:
|
|
34
|
+
<<: *default
|
|
35
|
+
database: active_admin_import_test
|
|
36
|
+
YAML
|
|
37
|
+
end
|
|
10
38
|
|
|
11
|
-
|
|
12
|
-
|
|
39
|
+
generate :model, 'author name:string{10}:uniq last_name:string birthday:date --force'
|
|
40
|
+
generate :model, 'post title:string:uniq body:text request_ip:string author:references --force'
|
|
41
|
+
generate :model, 'post_comment body:text post:references --force'
|
|
13
42
|
|
|
14
|
-
|
|
15
|
-
inject_into_file '
|
|
43
|
+
inject_into_file 'app/models/author.rb', " validates_presence_of :name\n validates_uniqueness_of :last_name\n", before: 'end'
|
|
44
|
+
inject_into_file 'app/models/post.rb', " validates_presence_of :author\n has_many :post_comments\n", before: 'end'
|
|
16
45
|
|
|
17
|
-
|
|
46
|
+
# Add our local Active Admin to the load path (Rails 7.1+)
|
|
47
|
+
gsub_file "config/environment.rb",
|
|
48
|
+
'require_relative "application"',
|
|
49
|
+
"require_relative \"application\"\n$LOAD_PATH.unshift('#{File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib'))}')\nrequire \"active_admin\"\n"
|
|
18
50
|
|
|
19
51
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
|
20
52
|
|
|
53
|
+
aa_v4 = ENV['AA']&.start_with?('4')
|
|
54
|
+
|
|
21
55
|
generate :'active_admin:install --skip-users'
|
|
22
|
-
generate :'formtastic:install'
|
|
23
56
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
26
66
|
|
|
67
|
+
run 'rm -rf test'
|
|
27
68
|
route "root :to => 'admin/dashboard#index'"
|
|
69
|
+
rake 'db:create db:migrate'
|
|
28
70
|
|
|
29
|
-
|
|
71
|
+
run 'rm -f Gemfile Gemfile.lock'
|
|
@@ -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,7 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
require_relative '../spec/support/test_app_paths'
|
|
2
|
+
|
|
3
|
+
desc "Creates a test rails app for the specs to run against"
|
|
3
4
|
task :setup do
|
|
4
5
|
require 'rails/version'
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
db = ENV['DB'] || 'sqlite'
|
|
8
|
+
rails_db = case db
|
|
9
|
+
when 'mysql' then 'mysql'
|
|
10
|
+
when 'postgres', 'postgresql' then 'postgresql'
|
|
11
|
+
else 'sqlite3'
|
|
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}"
|
|
16
|
+
|
|
17
|
+
rails_new_opts = %W(
|
|
18
|
+
--skip-turbolinks
|
|
19
|
+
--skip-spring
|
|
20
|
+
--skip-bootsnap
|
|
21
|
+
-d #{rails_db}
|
|
22
|
+
-m
|
|
23
|
+
spec/support/rails_template.rb
|
|
24
|
+
)
|
|
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
|
+
rails_new_opts.unshift('--skip-asset-pipeline') if aa_v4
|
|
28
|
+
|
|
29
|
+
system "bundle exec rails new spec/rails/#{TestAppPaths.app_dir_name} #{rails_new_opts.join(' ')}"
|
|
7
30
|
end
|