Build Status Coverage Code Climate Gem Version License
The fastest and most efficient CSV import for Active Admin with support for validations, bulk inserts, and encoding handling.
Add this line to your application's Gemfile:
gem "active_admin_import"
or
gem "active_admin_import" , github: "activeadmin-plugins/active_admin_import"
And then execute:
$ bundle
- Replacements/Updates support
- Encoding handling
- CSV options
- Ability to describe/change CSV headers
- Bulk import (activerecord-import)
- Callbacks
- Zip files
- and more...
ActiveAdmin.register Post do active_admin_import options end
| Tool | Description |
|---|---|
| :back | resource action to redirect after processing |
| :csv_options | hash with column separator, row separator, etc |
| :validate | bool (true by default), perform validations or not |
| :batch_transaction | bool (false by default), if transaction is used when batch importing and works when :validate is set to true |
| :batch_size | integer value of max record count inserted by 1 query/transaction |
| :before_import | proc for before import action, hook called with importer object |
| :after_import | proc for after import action, hook called with importer object |
| :before_batch_import | proc for before each batch action, called with importer object |
| :after_batch_import | proc for after each batch action, called with importer object |
| :on_duplicate_key_update | an Array or Hash, tells activerecord-import to use MySQL's ON DUPLICATE KEY UPDATE or Postgres 9.5+/SQLite 3.24.0+ ON CONFLICT DO UPDATE ability |
| :on_duplicate_key_ignore | bool, tells activerecord-import to use MySQL's INSERT IGNORE or Postgres 9.5+ ON CONFLICT DO NOTHING or SQLite's INSERT OR IGNORE ability |
| :ignore | bool, alias for on_duplicate_key_ignore |
| :timestamps | bool, tells activerecord-import to not add timestamps (if false) even if record timestamps is disabled in ActiveRecord::Base |
| :template | custom template rendering |
| :template_object | object passing to view |
| :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). |
| :resource_class | resource class name |
| :resource_label | resource label value |
| :plural_resource_label | pluralized resource label value (default config.plural_resource_label) |
| :error_limit | Limit the number of errors reported (default 5, set to nil for all) |
| :headers_rewrites | hash with key (csv header) - value (db column name) rows mapping |
| :if | Controls whether the 'Import' button is displayed. It supports a proc to be evaluated into a boolean value within the activeadmin render context. |
| :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.). |
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:
class ImportResultWithIds < ActiveAdminImport::ImportResult attr_reader :ids def initialize super @ids = [] end def add(batch_result, qty) super @ids.concat(Array(batch_result.ids)) end end ActiveAdmin.register Author do active_admin_import result_class: ImportResultWithIds do |result, options| EnqueueAuthorsJob.perform_later(result.ids) if result.imported? instance_exec(result, options, &ActiveAdminImport::DSL::DEFAULT_RESULT_PROC) end end
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.
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.
The current user must be authorized to perform imports. With CanCanCan:
class Ability include CanCan::Ability def initialize(user) can :import, Post end end
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:
ActiveAdmin.register PostComment do belongs_to :post controller do def active_admin_import_context { post_id: parent.id, request_ip: request.remote_ip } end end active_admin_import before_batch_import: ->(importer) { importer.csv_lines.map! { |row| row << importer.model.post_id } importer.headers.merge!(:'Post Id' => :post_id) } end
ActiveAdmin.register Post do active_admin_import validate: true, template_object: ActiveAdminImport::Model.new( hint: "expected header order: body, title, author", csv_headers: %w[body title author] ) end
ActiveAdmin.register Post do active_admin_import validate: true, template_object: ActiveAdminImport::Model.new(force_encoding: :auto) end
ActiveAdmin.register Post do active_admin_import validate: true, template_object: ActiveAdminImport::Model.new( hint: "file is encoded in ISO-8859-1", force_encoding: "ISO-8859-1" ) end
ActiveAdmin.register Post do active_admin_import validate: true, template_object: ActiveAdminImport::Model.new( hint: "upload a CSV file", allow_archive: false ) end
Useful when the CSV file has columns that don't exist on the table. Available since 3.1.0.
ActiveAdmin.register Post do active_admin_import before_batch_import: ->(importer) { importer.batch_slice_columns(['name', 'last_name']) } end
Tip: pass Post.column_names to keep only the columns that exist on the table.
Replace an Author name column in the CSV with the matching author_id before insert:
ActiveAdmin.register Post do active_admin_import validate: true, headers_rewrites: { 'Author name': :author_id }, before_batch_import: ->(importer) { names = importer.values_at(:author_id) mapping = Author.where(name: names).pluck(:name, :id).to_h importer.batch_replace(:author_id, mapping) } end
Two strategies, depending on your database and whether you need validations.
On databases that support upserts (MySQL, PostgreSQL 9.5+, SQLite 3.24+),
:on_duplicate_key_update updates colliding rows and inserts new ones in a
single statement — no extra delete. The option is passed straight to
activerecord-import, so its shape depends on the adapter:
# PostgreSQL / SQLite on_duplicate_key_update: { conflict_target: [:id], columns: %i[name last_name birthday] } # MySQL (infers the key from the columns) on_duplicate_key_update: %i[name last_name birthday]
ActiveAdmin.register Author do active_admin_import validate: false, on_duplicate_key_update: { conflict_target: [:id], columns: %i[name last_name birthday] } end
Notes:
- Only the columns you list are updated; other columns on the existing row keep their values.
- Use
validate: false—activerecord-importruns uniqueness validations against the very rows the upsert is about to overwrite, sovalidates_uniqueness_ofwould otherwise reject the update. - Active Record callbacks are not fired for bulk imports.
When you can't rely on upsert support — an older database, or you need your model
validations to run — delete the colliding rows just before each batch insert.
The old row is gone before the insert, so validates_uniqueness_of doesn't trip,
at the cost of a second query and full-row replacement (columns absent from
the CSV are reset, not preserved):
ActiveAdmin.register Post do active_admin_import before_batch_import: ->(importer) { Post.where(id: importer.values_at('id')).delete_all } end
ActiveAdmin.register Post do active_admin_import validate: false, csv_options: { col_sep: ";" }, batch_size: 1000 end
ActiveAdmin.register Post do active_admin_import validate: false, csv_options: { col_sep: ";" }, resource_class: ImportedPost, # write to a staging table before_import: ->(_) { ImportedPost.delete_all }, after_import: ->(_) { Post.transaction do Post.delete_all Post.connection.execute("INSERT INTO posts (SELECT * FROM imported_posts)") end }, back: ->(_) { config.namespace.resource_for(Post).route_collection_path } end
ActiveAdmin.register Post do active_admin_import validate: false, template: 'admin/posts/import', template_object: ActiveAdminImport::Model.new( hint: "you can configure CSV options", csv_options: { col_sep: ";", row_sep: nil, quote_char: nil } ) end
app/views/admin/posts/import.html.erb:
<p><%= raw(@active_admin_import_model.hint) %></p> <%= semantic_form_for @active_admin_import_model, url: { action: :do_import }, html: { multipart: true } do |f| %> <%= f.inputs do %> <%= f.input :file, as: :file %> <% end %> <%= f.inputs "CSV options", for: [:csv_options, OpenStruct.new(@active_admin_import_model.csv_options)] do |csv| %> <% csv.with_options input_html: { style: 'width:40px;' } do |opts| %> <%= opts.input :col_sep %> <%= opts.input :row_sep %> <%= opts.input :quote_char %> <% end %> <% end %> <%= f.actions do %> <%= f.action :submit, label: t("active_admin_import.import_btn"), button_html: { disable_with: t("active_admin_import.import_btn_disabled") } %> <% end %> <% end %>
Both before_batch_import and after_batch_import receive the Importer instance:
active_admin_import before_batch_import: ->(importer) { importer.file # the uploaded file importer.resource # the ActiveRecord class being imported into importer.options # the resolved options hash importer.headers # CSV headers (mutable) importer.csv_lines # parsed CSV rows for the current batch (mutable) importer.model # the template_object instance }
| Tool | Description |
|---|---|
| rchardet | Character encoding auto-detection in Ruby. As smart as your browser. Open source. |
| activerecord-import | Powerful library for bulk inserting data using ActiveRecord. |
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request