active_admin_import 5.0.0 → 6.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 +162 -0
- data/Gemfile +18 -12
- data/README.md +254 -17
- data/Rakefile +1 -4
- data/active_admin_import.gemspec +4 -4
- data/lib/active_admin_import/dsl.rb +22 -5
- data/lib/active_admin_import/import_result.rb +11 -1
- data/lib/active_admin_import/importer.rb +3 -2
- data/lib/active_admin_import/model.rb +4 -1
- data/lib/active_admin_import/options.rb +2 -1
- data/lib/active_admin_import/version.rb +1 -1
- data/spec/fixtures/files/author_invalid_format.txt +2 -0
- data/spec/fixtures/files/authors_values_exceeded_headers.csv +3 -0
- data/spec/fixtures/files/post_comments.csv +3 -0
- data/spec/import_spec.rb +206 -1
- data/spec/spec_helper.rb +13 -17
- data/spec/support/admin.rb +16 -0
- data/spec/support/rails_template.rb +49 -18
- data/tasks/test.rake +18 -4
- metadata +20 -39
- data/.travis.yml +0 -14
- data/CHANGELOG.md +0 -33
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bdc784c6f8a845b491b0dc93e44b073b548bccc182bb3cf6c26cb9f80e59b587
|
|
4
|
+
data.tar.gz: 798237101d7d4403ed8fb1d1d4ed466b4b78bc44740e20137887d2c8b6464e3f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '06639b5514e0c5af1f2cfd70f66f7de10b798fc22f9153b4870c0859b3f95fd372cf73f406b15b7df8b3e4d6fc350a911680248e09d15412479c55f092869a0d'
|
|
7
|
+
data.tar.gz: 1b49c36c911a6eae50d653eafa4063a139b5d901b81264b213ece4bb995158bf7685f62facce2d5d1a69b76fe2735483296a26f9f697f6fad802d562ca3deef5
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
pages: write
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }} / AA ${{ matrix.activeadmin }} / SQLite
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
ruby: ['3.2', '3.3', '3.4']
|
|
20
|
+
rails: ['7.1.0', '7.2.0', '8.0.0']
|
|
21
|
+
activeadmin: ['3.2.0', '3.3.0', '3.4.0', '3.5.1']
|
|
22
|
+
exclude:
|
|
23
|
+
- rails: '8.0.0'
|
|
24
|
+
activeadmin: '3.2.0'
|
|
25
|
+
env:
|
|
26
|
+
RAILS: ${{ matrix.rails }}
|
|
27
|
+
AA: ${{ matrix.activeadmin }}
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
- uses: ruby/setup-ruby@v1
|
|
31
|
+
with:
|
|
32
|
+
ruby-version: ${{ matrix.ruby }}
|
|
33
|
+
bundler-cache: true
|
|
34
|
+
- name: Run tests
|
|
35
|
+
run: bundle exec rspec spec
|
|
36
|
+
test-mysql:
|
|
37
|
+
name: Ruby 3.4 / Rails 8.0.0 / AA 3.5.1 / MySQL 8.0
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
env:
|
|
40
|
+
RAILS: '8.0.0'
|
|
41
|
+
AA: '3.5.1'
|
|
42
|
+
DB: mysql
|
|
43
|
+
DB_HOST: 127.0.0.1
|
|
44
|
+
DB_PORT: 3306
|
|
45
|
+
DB_USERNAME: root
|
|
46
|
+
DB_PASSWORD: root
|
|
47
|
+
services:
|
|
48
|
+
mysql:
|
|
49
|
+
image: mysql:8.0
|
|
50
|
+
env:
|
|
51
|
+
MYSQL_ROOT_PASSWORD: root
|
|
52
|
+
MYSQL_DATABASE: active_admin_import_test
|
|
53
|
+
ports:
|
|
54
|
+
- 3306:3306
|
|
55
|
+
options: >-
|
|
56
|
+
--health-cmd="mysqladmin ping -h localhost -uroot -proot"
|
|
57
|
+
--health-interval=10s
|
|
58
|
+
--health-timeout=5s
|
|
59
|
+
--health-retries=10
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/checkout@v4
|
|
62
|
+
- uses: ruby/setup-ruby@v1
|
|
63
|
+
with:
|
|
64
|
+
ruby-version: '3.4'
|
|
65
|
+
bundler-cache: true
|
|
66
|
+
- name: Run tests
|
|
67
|
+
run: bundle exec rspec spec
|
|
68
|
+
test-postgres:
|
|
69
|
+
name: Ruby 3.4 / Rails 8.0.0 / AA 3.5.1 / PostgreSQL 16
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
env:
|
|
72
|
+
RAILS: '8.0.0'
|
|
73
|
+
AA: '3.5.1'
|
|
74
|
+
DB: postgres
|
|
75
|
+
DB_HOST: 127.0.0.1
|
|
76
|
+
DB_PORT: 5432
|
|
77
|
+
DB_USERNAME: postgres
|
|
78
|
+
DB_PASSWORD: postgres
|
|
79
|
+
services:
|
|
80
|
+
postgres:
|
|
81
|
+
image: postgres:16
|
|
82
|
+
env:
|
|
83
|
+
POSTGRES_USER: postgres
|
|
84
|
+
POSTGRES_PASSWORD: postgres
|
|
85
|
+
POSTGRES_DB: active_admin_import_test
|
|
86
|
+
ports:
|
|
87
|
+
- 5432:5432
|
|
88
|
+
options: >-
|
|
89
|
+
--health-cmd="pg_isready -U postgres"
|
|
90
|
+
--health-interval=10s
|
|
91
|
+
--health-timeout=5s
|
|
92
|
+
--health-retries=10
|
|
93
|
+
steps:
|
|
94
|
+
- uses: actions/checkout@v4
|
|
95
|
+
- uses: ruby/setup-ruby@v1
|
|
96
|
+
with:
|
|
97
|
+
ruby-version: '3.4'
|
|
98
|
+
bundler-cache: true
|
|
99
|
+
- name: Run tests
|
|
100
|
+
run: bundle exec rspec spec
|
|
101
|
+
coverage:
|
|
102
|
+
name: Coverage
|
|
103
|
+
runs-on: ubuntu-latest
|
|
104
|
+
steps:
|
|
105
|
+
- uses: actions/checkout@v4
|
|
106
|
+
- uses: ruby/setup-ruby@v1
|
|
107
|
+
with:
|
|
108
|
+
ruby-version: '3.4'
|
|
109
|
+
bundler-cache: true
|
|
110
|
+
- name: Run tests with coverage
|
|
111
|
+
run: bundle exec rspec spec
|
|
112
|
+
- name: Upload coverage
|
|
113
|
+
uses: actions/upload-artifact@v4
|
|
114
|
+
with:
|
|
115
|
+
name: coverage
|
|
116
|
+
path: coverage/
|
|
117
|
+
|
|
118
|
+
- name: Generate badge.json
|
|
119
|
+
run: |
|
|
120
|
+
LAST_RUN="coverage/.last_run.json"
|
|
121
|
+
if [ ! -f "$LAST_RUN" ]; then
|
|
122
|
+
mkdir -p badge
|
|
123
|
+
echo '{"schemaVersion":1,"label":"coverage","message":"unknown","color":"lightgrey"}' > badge/badge.json
|
|
124
|
+
exit 0
|
|
125
|
+
fi
|
|
126
|
+
PERCENT=$(ruby -rjson -e "puts JSON.parse(File.read('$LAST_RUN')).dig('result','line').round(1)")
|
|
127
|
+
PERCENT_NUM=$(ruby -rjson -e "puts JSON.parse(File.read('$LAST_RUN')).dig('result','line')")
|
|
128
|
+
if ruby -e "exit(($PERCENT_NUM >= 90) ? 0 : 1)"; then COLOR="brightgreen"
|
|
129
|
+
elif ruby -e "exit(($PERCENT_NUM >= 75) ? 0 : 1)"; then COLOR="green"
|
|
130
|
+
elif ruby -e "exit(($PERCENT_NUM >= 60) ? 0 : 1)"; then COLOR="yellow"
|
|
131
|
+
else COLOR="red"; fi
|
|
132
|
+
mkdir -p badge
|
|
133
|
+
echo "{\"schemaVersion\":1,\"label\":\"coverage\",\"message\":\"${PERCENT}%\",\"color\":\"${COLOR}\"}" > badge/badge.json
|
|
134
|
+
|
|
135
|
+
- name: Upload badge artifact
|
|
136
|
+
uses: actions/upload-artifact@v4
|
|
137
|
+
with:
|
|
138
|
+
name: coverage-badge
|
|
139
|
+
path: badge
|
|
140
|
+
|
|
141
|
+
deploy-coverage:
|
|
142
|
+
needs: coverage
|
|
143
|
+
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
|
144
|
+
runs-on: ubuntu-latest
|
|
145
|
+
environment:
|
|
146
|
+
name: github-pages
|
|
147
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
148
|
+
steps:
|
|
149
|
+
- name: Download coverage badge
|
|
150
|
+
uses: actions/download-artifact@v4
|
|
151
|
+
with:
|
|
152
|
+
name: coverage-badge
|
|
153
|
+
path: .
|
|
154
|
+
- name: Setup Pages
|
|
155
|
+
uses: actions/configure-pages@v5
|
|
156
|
+
- name: Upload Pages artifact
|
|
157
|
+
uses: actions/upload-pages-artifact@v3
|
|
158
|
+
with:
|
|
159
|
+
path: .
|
|
160
|
+
- name: Deploy to GitHub Pages
|
|
161
|
+
id: deployment
|
|
162
|
+
uses: actions/deploy-pages@v4
|
data/Gemfile
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
1
|
source 'https://rubygems.org'
|
|
3
|
-
|
|
4
|
-
# Specify your gem's dependencies in active_admin_importable.gemspec
|
|
5
2
|
gemspec
|
|
6
3
|
|
|
4
|
+
default_rails_version = '7.1.0'
|
|
5
|
+
default_activeadmin_version = '3.2.0'
|
|
6
|
+
|
|
7
|
+
gem 'rails', "~> #{ENV['RAILS'] || default_rails_version}"
|
|
8
|
+
gem 'activeadmin', "~> #{ENV['AA'] || default_activeadmin_version}"
|
|
9
|
+
gem 'sprockets-rails'
|
|
10
|
+
gem 'sass-rails'
|
|
7
11
|
|
|
8
12
|
group :test do
|
|
9
|
-
|
|
10
|
-
rails_version = ENV['RAILS'] || default_rails_version
|
|
11
|
-
gem 'sassc-rails'
|
|
12
|
-
gem 'rails', rails_version
|
|
13
|
+
gem 'simplecov', require: false
|
|
13
14
|
gem 'rspec-rails'
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
case ENV['DB']
|
|
16
|
+
when 'mysql'
|
|
17
|
+
gem 'mysql2'
|
|
18
|
+
when 'postgres', 'postgresql'
|
|
19
|
+
gem 'pg'
|
|
20
|
+
else
|
|
21
|
+
gem 'sqlite3', '~> 2.0'
|
|
22
|
+
end
|
|
17
23
|
gem 'database_cleaner'
|
|
18
24
|
gem 'capybara'
|
|
19
|
-
gem '
|
|
20
|
-
gem '
|
|
25
|
+
gem 'cuprite'
|
|
26
|
+
gem 'webrick', require: false
|
|
21
27
|
end
|
data/README.md
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
# ActiveAdminImport
|
|
2
2
|
|
|
3
|
-
[![
|
|
4
|
-
|
|
5
|
-
[![Code Climate
|
|
6
|
-
[![Gem Version
|
|
7
|
-
[![License
|
|
3
|
+
[![Build Status][build_badge]][build_link]
|
|
4
|
+
![Coverage][coverage_badge]
|
|
5
|
+
[![Code Climate][codeclimate_badge]][codeclimate_link]
|
|
6
|
+
[![Gem Version][rubygems_badge]][rubygems_link]
|
|
7
|
+
[![License][license_badge]][license_link]
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
The
|
|
11
|
-
|
|
12
|
-
For more about ActiveAdminImport installation and usage, check [Documentation website](http://activeadmin-plugins.github.io/active_admin_import/) and [Wiki pages](https://github.com/activeadmin-plugins/active_admin_import/wiki) for some specific cases and caveats.
|
|
10
|
+
The fastest and most efficient CSV import for Active Admin with support for validations, bulk inserts, and encoding handling.
|
|
13
11
|
|
|
14
12
|
|
|
15
13
|
## Installation
|
|
@@ -44,7 +42,7 @@ And then execute:
|
|
|
44
42
|
#### Basic usage
|
|
45
43
|
|
|
46
44
|
```ruby
|
|
47
|
-
ActiveAdmin.register Post
|
|
45
|
+
ActiveAdmin.register Post do
|
|
48
46
|
active_admin_import options
|
|
49
47
|
end
|
|
50
48
|
```
|
|
@@ -68,6 +66,7 @@ Tool | Description
|
|
|
68
66
|
:timestamps |bool, tells activerecord-import to not add timestamps (if false) even if record timestamps is disabled in ActiveRecord::Base
|
|
69
67
|
:template |custom template rendering
|
|
70
68
|
:template_object |object passing to view
|
|
69
|
+
:result_class |custom `ImportResult` subclass to collect data from each batch (e.g. inserted ids). Must respond to `add(batch_result, qty)` plus the readers used in flash messages (`failed`, `total`, `imported_qty`, `imported?`, `failed?`, `empty?`, `failed_message`).
|
|
71
70
|
:resource_class |resource class name
|
|
72
71
|
:resource_label |resource label value
|
|
73
72
|
:plural_resource_label |pluralized resource label value (default config.plural_resource_label)
|
|
@@ -77,9 +76,248 @@ Tool | Description
|
|
|
77
76
|
|
|
78
77
|
|
|
79
78
|
|
|
80
|
-
####
|
|
79
|
+
#### Custom ImportResult
|
|
80
|
+
|
|
81
|
+
To collect extra data from each batch (for example the ids of inserted rows so you can enqueue background jobs against them), pass a subclass of `ActiveAdminImport::ImportResult` via `:result_class`:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
class ImportResultWithIds < ActiveAdminImport::ImportResult
|
|
85
|
+
attr_reader :ids
|
|
86
|
+
|
|
87
|
+
def initialize
|
|
88
|
+
super
|
|
89
|
+
@ids = []
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def add(batch_result, qty)
|
|
93
|
+
super
|
|
94
|
+
@ids.concat(Array(batch_result.ids))
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
ActiveAdmin.register Author do
|
|
99
|
+
active_admin_import result_class: ImportResultWithIds do |result, options|
|
|
100
|
+
EnqueueAuthorsJob.perform_later(result.ids) if result.imported?
|
|
101
|
+
instance_exec(result, options, &ActiveAdminImport::DSL::DEFAULT_RESULT_PROC)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The action block is invoked via `instance_exec` with `result` and `options` as block arguments, so you can either capture them with `do |result, options|` or read them as locals when no arguments are declared.
|
|
107
|
+
|
|
108
|
+
Note: which batch-result attributes are populated depends on the database adapter and the import options. `activerecord-import` returns ids reliably on PostgreSQL; on MySQL/SQLite the behavior depends on the adapter and options like `on_duplicate_key_update`. Putting the collection logic in your own subclass keeps these adapter quirks in your application code.
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
#### Authorization
|
|
112
|
+
|
|
113
|
+
The current user must be authorized to perform imports. With CanCanCan:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
class Ability
|
|
117
|
+
include CanCan::Ability
|
|
118
|
+
|
|
119
|
+
def initialize(user)
|
|
120
|
+
can :import, Post
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
#### Per-request context
|
|
127
|
+
|
|
128
|
+
Define an `active_admin_import_context` method on the controller to inject request-derived attributes into every import (current user, parent resource id, request IP, etc.). The returned hash is merged into the import model after form params, so it always wins for the keys it provides:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
ActiveAdmin.register PostComment do
|
|
132
|
+
belongs_to :post
|
|
133
|
+
|
|
134
|
+
controller do
|
|
135
|
+
def active_admin_import_context
|
|
136
|
+
{ post_id: parent.id, request_ip: request.remote_ip }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
active_admin_import before_batch_import: ->(importer) {
|
|
141
|
+
importer.csv_lines.map! { |row| row << importer.model.post_id }
|
|
142
|
+
importer.headers.merge!(:'Post Id' => :post_id)
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
#### Examples
|
|
149
|
+
|
|
150
|
+
##### Files without CSV headers
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
ActiveAdmin.register Post do
|
|
154
|
+
active_admin_import validate: true,
|
|
155
|
+
template_object: ActiveAdminImport::Model.new(
|
|
156
|
+
hint: "expected header order: body, title, author",
|
|
157
|
+
csv_headers: %w[body title author]
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
##### Auto-detect file encoding
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
ActiveAdmin.register Post do
|
|
166
|
+
active_admin_import validate: true,
|
|
167
|
+
template_object: ActiveAdminImport::Model.new(force_encoding: :auto)
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
##### Force a specific (non-UTF-8) encoding
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
ActiveAdmin.register Post do
|
|
175
|
+
active_admin_import validate: true,
|
|
176
|
+
template_object: ActiveAdminImport::Model.new(
|
|
177
|
+
hint: "file is encoded in ISO-8859-1",
|
|
178
|
+
force_encoding: "ISO-8859-1"
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
##### Disallow ZIP upload
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
ActiveAdmin.register Post do
|
|
187
|
+
active_admin_import validate: true,
|
|
188
|
+
template_object: ActiveAdminImport::Model.new(
|
|
189
|
+
hint: "upload a CSV file",
|
|
190
|
+
allow_archive: false
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
##### Skip CSV columns
|
|
196
|
+
|
|
197
|
+
Useful when the CSV file has columns that don't exist on the table. Available since 3.1.0.
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
ActiveAdmin.register Post do
|
|
201
|
+
active_admin_import before_batch_import: ->(importer) {
|
|
202
|
+
importer.batch_slice_columns(['name', 'last_name'])
|
|
203
|
+
}
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Tip: pass `Post.column_names` to keep only the columns that exist on the table.
|
|
208
|
+
|
|
209
|
+
##### Resolve associations on the fly
|
|
210
|
+
|
|
211
|
+
Replace an `Author name` column in the CSV with the matching `author_id` before insert:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
ActiveAdmin.register Post do
|
|
215
|
+
active_admin_import validate: true,
|
|
216
|
+
headers_rewrites: { 'Author name': :author_id },
|
|
217
|
+
before_batch_import: ->(importer) {
|
|
218
|
+
names = importer.values_at(:author_id)
|
|
219
|
+
mapping = Author.where(name: names).pluck(:name, :id).to_h
|
|
220
|
+
importer.batch_replace(:author_id, mapping)
|
|
221
|
+
}
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
##### Update existing records by id
|
|
226
|
+
|
|
227
|
+
Delete colliding rows just before each batch insert:
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
ActiveAdmin.register Post do
|
|
231
|
+
active_admin_import before_batch_import: ->(importer) {
|
|
232
|
+
Post.where(id: importer.values_at('id')).delete_all
|
|
233
|
+
}
|
|
234
|
+
end
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
For databases that support upserts you can use `:on_duplicate_key_update` instead.
|
|
238
|
+
|
|
239
|
+
##### Tune batch size
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
ActiveAdmin.register Post do
|
|
243
|
+
active_admin_import validate: false,
|
|
244
|
+
csv_options: { col_sep: ";" },
|
|
245
|
+
batch_size: 1000
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
##### Import into an intermediate table
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
ActiveAdmin.register Post do
|
|
253
|
+
active_admin_import validate: false,
|
|
254
|
+
csv_options: { col_sep: ";" },
|
|
255
|
+
resource_class: ImportedPost, # write to a staging table
|
|
256
|
+
before_import: ->(_) { ImportedPost.delete_all },
|
|
257
|
+
after_import: ->(_) {
|
|
258
|
+
Post.transaction do
|
|
259
|
+
Post.delete_all
|
|
260
|
+
Post.connection.execute("INSERT INTO posts (SELECT * FROM imported_posts)")
|
|
261
|
+
end
|
|
262
|
+
},
|
|
263
|
+
back: ->(_) { config.namespace.resource_for(Post).route_collection_path }
|
|
264
|
+
end
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
##### Allow user input for CSV options (custom template)
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
ActiveAdmin.register Post do
|
|
271
|
+
active_admin_import validate: false,
|
|
272
|
+
template: 'admin/posts/import',
|
|
273
|
+
template_object: ActiveAdminImport::Model.new(
|
|
274
|
+
hint: "you can configure CSV options",
|
|
275
|
+
csv_options: { col_sep: ";", row_sep: nil, quote_char: nil }
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`app/views/admin/posts/import.html.erb`:
|
|
281
|
+
|
|
282
|
+
```erb
|
|
283
|
+
<p><%= raw(@active_admin_import_model.hint) %></p>
|
|
284
|
+
|
|
285
|
+
<%= semantic_form_for @active_admin_import_model, url: { action: :do_import }, html: { multipart: true } do |f| %>
|
|
286
|
+
<%= f.inputs do %>
|
|
287
|
+
<%= f.input :file, as: :file %>
|
|
288
|
+
<% end %>
|
|
289
|
+
|
|
290
|
+
<%= f.inputs "CSV options", for: [:csv_options, OpenStruct.new(@active_admin_import_model.csv_options)] do |csv| %>
|
|
291
|
+
<% csv.with_options input_html: { style: 'width:40px;' } do |opts| %>
|
|
292
|
+
<%= opts.input :col_sep %>
|
|
293
|
+
<%= opts.input :row_sep %>
|
|
294
|
+
<%= opts.input :quote_char %>
|
|
295
|
+
<% end %>
|
|
296
|
+
<% end %>
|
|
297
|
+
|
|
298
|
+
<%= f.actions do %>
|
|
299
|
+
<%= f.action :submit,
|
|
300
|
+
label: t("active_admin_import.import_btn"),
|
|
301
|
+
button_html: { disable_with: t("active_admin_import.import_btn_disabled") } %>
|
|
302
|
+
<% end %>
|
|
303
|
+
<% end %>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
##### Inspecting the importer in batch callbacks
|
|
307
|
+
|
|
308
|
+
Both `before_batch_import` and `after_batch_import` receive the `Importer` instance:
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
active_admin_import before_batch_import: ->(importer) {
|
|
312
|
+
importer.file # the uploaded file
|
|
313
|
+
importer.resource # the ActiveRecord class being imported into
|
|
314
|
+
importer.options # the resolved options hash
|
|
315
|
+
importer.headers # CSV headers (mutable)
|
|
316
|
+
importer.csv_lines # parsed CSV rows for the current batch (mutable)
|
|
317
|
+
importer.model # the template_object instance
|
|
318
|
+
}
|
|
319
|
+
```
|
|
81
320
|
|
|
82
|
-
[Check various examples](https://github.com/activeadmin-plugins/active_admin_import/wiki)
|
|
83
321
|
|
|
84
322
|
## Dependencies
|
|
85
323
|
|
|
@@ -91,16 +329,15 @@ Tool | Description
|
|
|
91
329
|
[rchardet]: https://github.com/jmhodges/rchardet
|
|
92
330
|
[activerecord-import]: https://github.com/zdennis/activerecord-import
|
|
93
331
|
|
|
94
|
-
[build_badge]: https://
|
|
95
|
-
[build_link]: https://
|
|
96
|
-
[
|
|
97
|
-
[coveralls_link]: https://coveralls.io/github/activeadmin-plugins/active_admin_import
|
|
332
|
+
[build_badge]: https://github.com/activeadmin-plugins/active_admin_import/actions/workflows/test.yml/badge.svg
|
|
333
|
+
[build_link]: https://github.com/activeadmin-plugins/active_admin_import/actions
|
|
334
|
+
[coverage_badge]: https://img.shields.io/endpoint?url=https://activeadmin-plugins.github.io/active_admin_import/badge.json
|
|
98
335
|
[codeclimate_badge]: https://codeclimate.com/github/activeadmin-plugins/active_admin_import/badges/gpa.svg
|
|
99
336
|
[codeclimate_link]: https://codeclimate.com/github/activeadmin-plugins/active_admin_import
|
|
100
337
|
[rubygems_badge]: https://badge.fury.io/rb/active_admin_import.svg
|
|
101
338
|
[rubygems_link]: https://rubygems.org/gems/active_admin_import
|
|
102
|
-
[license_badge]:
|
|
103
|
-
[license_link]:
|
|
339
|
+
[license_badge]: https://img.shields.io/:license-mit-blue.svg
|
|
340
|
+
[license_link]: https://Fivell.mit-license.org
|
|
104
341
|
|
|
105
342
|
|
|
106
343
|
## Contributing
|
data/Rakefile
CHANGED
data/active_admin_import.gemspec
CHANGED
|
@@ -7,16 +7,16 @@ Gem::Specification.new do |gem|
|
|
|
7
7
|
gem.email = ['fedoronchuk@gmail.com']
|
|
8
8
|
gem.description = 'The most efficient way to import for Active Admin'
|
|
9
9
|
gem.summary = 'ActiveAdmin import based on activerecord-import gem.'
|
|
10
|
-
gem.homepage = '
|
|
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
13
|
gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
|
|
13
14
|
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
|
14
|
-
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
15
15
|
gem.name = 'active_admin_import'
|
|
16
16
|
gem.require_paths = ['lib']
|
|
17
17
|
gem.version = ActiveAdminImport::VERSION
|
|
18
|
-
gem.add_runtime_dependency 'activerecord-import', '>= 0
|
|
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', '>=
|
|
21
|
+
gem.add_dependency 'activeadmin', '>= 3.0', '< 4.0'
|
|
22
22
|
end
|
|
@@ -25,6 +25,22 @@ 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
|
+
def self.prepare_import_model(template_object, controller, params: nil)
|
|
31
|
+
model = template_object.is_a?(Proc) ? template_object.call : template_object
|
|
32
|
+
if params
|
|
33
|
+
params_key = ActiveModel::Naming.param_key(model.class)
|
|
34
|
+
model.assign_attributes(params[params_key].try(:deep_symbolize_keys) || {})
|
|
35
|
+
end
|
|
36
|
+
return model unless controller.respond_to?(CONTEXT_METHOD, true)
|
|
37
|
+
context = controller.send(CONTEXT_METHOD)
|
|
38
|
+
return model unless context.is_a?(Hash)
|
|
39
|
+
context = context.merge(file: model.file) if model.respond_to?(:file) && !context.key?(:file)
|
|
40
|
+
model.assign_attributes(context)
|
|
41
|
+
model
|
|
42
|
+
end
|
|
43
|
+
|
|
28
44
|
DEFAULT_RESULT_PROC = lambda do |result, options|
|
|
29
45
|
model_name = options[:resource_label].downcase
|
|
30
46
|
plural_model_name = options[:plural_resource_label].downcase
|
|
@@ -54,11 +70,10 @@ module ActiveAdminImport
|
|
|
54
70
|
options.assert_valid_keys(*Options::VALID_OPTIONS)
|
|
55
71
|
|
|
56
72
|
options = Options.options_for(config, options)
|
|
57
|
-
params_key = ActiveModel::Naming.param_key(options[:template_object])
|
|
58
73
|
|
|
59
74
|
collection_action :import, method: :get do
|
|
60
75
|
authorize!(ActiveAdminImport::Auth::IMPORT, active_admin_config.resource_class)
|
|
61
|
-
@active_admin_import_model = options[:template_object]
|
|
76
|
+
@active_admin_import_model = ActiveAdminImport::DSL.prepare_import_model(options[:template_object], self)
|
|
62
77
|
render template: options[:template]
|
|
63
78
|
end
|
|
64
79
|
|
|
@@ -75,8 +90,9 @@ module ActiveAdminImport
|
|
|
75
90
|
authorize!(ActiveAdminImport::Auth::IMPORT, active_admin_config.resource_class)
|
|
76
91
|
_params = params.respond_to?(:to_unsafe_h) ? params.to_unsafe_h : params
|
|
77
92
|
params = ActiveSupport::HashWithIndifferentAccess.new _params
|
|
78
|
-
@active_admin_import_model =
|
|
79
|
-
|
|
93
|
+
@active_admin_import_model = ActiveAdminImport::DSL.prepare_import_model(
|
|
94
|
+
options[:template_object], self, params: params
|
|
95
|
+
)
|
|
80
96
|
# go back to form
|
|
81
97
|
return render template: options[:template] unless @active_admin_import_model.valid?
|
|
82
98
|
@importer = Importer.new(
|
|
@@ -88,12 +104,13 @@ module ActiveAdminImport
|
|
|
88
104
|
result = @importer.import
|
|
89
105
|
|
|
90
106
|
if block_given?
|
|
91
|
-
|
|
107
|
+
instance_exec result, options, &block
|
|
92
108
|
else
|
|
93
109
|
instance_exec result, options, &DEFAULT_RESULT_PROC
|
|
94
110
|
end
|
|
95
111
|
rescue ActiveRecord::Import::MissingColumnError,
|
|
96
112
|
NoMethodError,
|
|
113
|
+
ArgumentError,
|
|
97
114
|
ActiveRecord::StatementInvalid,
|
|
98
115
|
CSV::MalformedCSVError,
|
|
99
116
|
ActiveAdminImport::Exception => e
|
|
@@ -33,11 +33,21 @@ module ActiveAdminImport
|
|
|
33
33
|
limit = options[:limit] || failed.count
|
|
34
34
|
failed.first(limit).map do |record|
|
|
35
35
|
errors = record.errors
|
|
36
|
-
failed_values = errors.
|
|
36
|
+
failed_values = attribute_names_for(errors).map do |key|
|
|
37
37
|
key == :base ? nil : record.public_send(key)
|
|
38
38
|
end
|
|
39
39
|
errors.full_messages.zip(failed_values).map { |ms| ms.compact.join(' - ') }.join(', ')
|
|
40
40
|
end.join(' ; ')
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def attribute_names_for(errors)
|
|
46
|
+
if Gem::Version.new(Rails.version) >= Gem::Version.new('7.0')
|
|
47
|
+
errors.attribute_names
|
|
48
|
+
else
|
|
49
|
+
errors.keys
|
|
50
|
+
end
|
|
51
|
+
end
|
|
42
52
|
end
|
|
43
53
|
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|
|
|
@@ -103,6 +104,8 @@ module ActiveAdminImport
|
|
|
103
104
|
|
|
104
105
|
def encode_file
|
|
105
106
|
data = File.read(file_path)
|
|
107
|
+
return if data.empty?
|
|
108
|
+
|
|
106
109
|
File.open(file_path, 'w') do |f|
|
|
107
110
|
f.write(encode(data))
|
|
108
111
|
end
|