rails_admin_import_no_encoding 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +48 -0
- data/MIT-LICENSE +20 -0
- data/README.md +476 -0
- data/Rakefile +17 -0
- data/app/views/rails_admin/main/_results.html.haml +18 -0
- data/app/views/rails_admin/main/_section.html.haml +10 -0
- data/app/views/rails_admin/main/import.html.haml +69 -0
- data/config/locales/README.md +3 -0
- data/config/locales/import.en.yml +50 -0
- data/lib/rails_admin_import/action.rb +46 -0
- data/lib/rails_admin_import/config/legacy_model.rb +42 -0
- data/lib/rails_admin_import/config/sections/import.rb +34 -0
- data/lib/rails_admin_import/config.rb +46 -0
- data/lib/rails_admin_import/eager_load.rb +3 -0
- data/lib/rails_admin_import/engine.rb +4 -0
- data/lib/rails_admin_import/formats/csv_importer.rb +81 -0
- data/lib/rails_admin_import/formats/dummy_importer.rb +16 -0
- data/lib/rails_admin_import/formats/file_importer.rb +47 -0
- data/lib/rails_admin_import/formats/json_importer.rb +32 -0
- data/lib/rails_admin_import/formats/xlsx_importer.rb +50 -0
- data/lib/rails_admin_import/formats.rb +34 -0
- data/lib/rails_admin_import/import_logger.rb +17 -0
- data/lib/rails_admin_import/import_model.rb +112 -0
- data/lib/rails_admin_import/importer.rb +248 -0
- data/lib/rails_admin_import/rails_admin_plugin.rb +23 -0
- data/lib/rails_admin_import/version.rb +3 -0
- data/lib/rails_admin_import.rb +23 -0
- metadata +115 -0
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
require 'rubygems/tasks'
|
4
|
+
Gem::Tasks.new
|
5
|
+
|
6
|
+
# Piggyback off the Rails Admin rake tasks to set up the CI environment
|
7
|
+
spec = Gem::Specification.find_by_name 'rails_admin'
|
8
|
+
Dir["#{spec.gem_dir}/lib/tasks/*.rake"].each { |rake| load rake }
|
9
|
+
|
10
|
+
require 'rspec/core/rake_task'
|
11
|
+
RSpec::Core::RakeTask.new(:spec)
|
12
|
+
|
13
|
+
task test: :spec
|
14
|
+
|
15
|
+
task :default do
|
16
|
+
system("bundle exec rake spec")
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
- if @results && @results[:success].any?
|
2
|
+
.alert.alert-success.form-horizontal.denser
|
3
|
+
%fieldset
|
4
|
+
%legend
|
5
|
+
%i.icon-chevron-right
|
6
|
+
= @results[:success_message]
|
7
|
+
%ul.control-group{style: 'display: none'}
|
8
|
+
- @results[:success].each do |message|
|
9
|
+
%li= message
|
10
|
+
- if @results && @results[:error].any?
|
11
|
+
.alert.alert-danger.form-horizontal.denser
|
12
|
+
%fieldset
|
13
|
+
%legend
|
14
|
+
%i.icon-chevron-down
|
15
|
+
= @results[:error_message]
|
16
|
+
%ul.control-group
|
17
|
+
- @results[:error].each do |message|
|
18
|
+
%li= message
|
@@ -0,0 +1,10 @@
|
|
1
|
+
- if fields.any?
|
2
|
+
.form-group.control-group
|
3
|
+
%label.col-sm-2.control-label= t("admin.import.#{section}")
|
4
|
+
.col-sm-10.controls
|
5
|
+
%ul.list-unstyled
|
6
|
+
- fields.each do |field|
|
7
|
+
%li
|
8
|
+
%label= capitalize_first_letter(field.label)
|
9
|
+
%p.help-block= t("admin.import.help.#{section}")
|
10
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
= render "results"
|
2
|
+
|
3
|
+
= form_tag import_path(@abstract_model), :multipart => true, class: 'form-horizontal denser' do
|
4
|
+
|
5
|
+
%input{name: "send_data", type: "hidden", value: "true"}/
|
6
|
+
%fieldset
|
7
|
+
%legend
|
8
|
+
%i.icon-chevron-down
|
9
|
+
= t('admin.import.legend.fields')
|
10
|
+
|
11
|
+
= render "section", section: "model_fields", fields: @import_model.model_fields
|
12
|
+
= render "section", section: "association_fields", fields: @import_model.association_fields
|
13
|
+
|
14
|
+
%fieldset
|
15
|
+
%legend
|
16
|
+
%i.icon-chevron-down
|
17
|
+
= t('admin.import.legend.upload')
|
18
|
+
.form-group.control-group
|
19
|
+
%label.col-sm-2.control-label{for: "file"}= t("admin.import.file")
|
20
|
+
.col-sm-10.controls
|
21
|
+
= file_field_tag :file, :class => "form-control"
|
22
|
+
%p.help-block= t('admin.import.help.file_limit', limit: RailsAdminImport.config.line_item_limit)
|
23
|
+
.form-group.control-group
|
24
|
+
%label.col-sm-2.control-label{for: "encoding"}= t("admin.import.encoding")
|
25
|
+
.col-sm-10.controls
|
26
|
+
= select_tag 'encoding',
|
27
|
+
options_for_select(Encoding.name_list.sort),
|
28
|
+
include_blank: true, data: { enumeration: true }
|
29
|
+
%p.help-block= t('admin.import.help.encoding', name: 'UTF-8')
|
30
|
+
.form-group.control-group
|
31
|
+
%label.col-sm-2.control-label= t("admin.import.update_if_exists")
|
32
|
+
.col-sm-10.controls
|
33
|
+
= check_box_tag :update_if_exists, '1', RailsAdminImport.config.update_if_exists, :class => "form-control"
|
34
|
+
%p.help-block= t('admin.import.help.update_if_exists')
|
35
|
+
.form-group.control-group
|
36
|
+
%label.col-sm-2.control-label{for: "update_lookup"}= t("admin.import.update_lookup")
|
37
|
+
.col-sm-10.controls
|
38
|
+
= select_tag 'update_lookup',
|
39
|
+
options_for_select(@import_model.update_lookup_field_names,
|
40
|
+
Array.wrap(@import_model.config.mapping_key).map(&:to_s)),
|
41
|
+
multiple: true,
|
42
|
+
data: { enumeration: true }
|
43
|
+
|
44
|
+
- unless @import_model.association_fields.empty?
|
45
|
+
%fieldset
|
46
|
+
%legend
|
47
|
+
%i.icon-chevron-down
|
48
|
+
= t('admin.import.legend.mapping')
|
49
|
+
|
50
|
+
- @import_model.association_fields.each do |field|
|
51
|
+
.form-group.control-group
|
52
|
+
%label.col-sm-2.control-label
|
53
|
+
= capitalize_first_letter(field.label)
|
54
|
+
= t("admin.import.mapping")
|
55
|
+
.col-sm-10.controls
|
56
|
+
= select_tag "associations[#{field.name}]",
|
57
|
+
options_for_select(@import_model.associated_model_fields(field),
|
58
|
+
Array.wrap(@import_model.associated_config(field).mapping_key).first.to_s),
|
59
|
+
data: { enumeration: true }
|
60
|
+
|
61
|
+
%br
|
62
|
+
.form-actions
|
63
|
+
%input{type: :hidden, name: 'return_to', value: (request.params[:return_to].presence || request.referer)}
|
64
|
+
%button.btn.btn-primary{type: "submit", name: "commit", data: {disable_with: "Uploading..."} }
|
65
|
+
%i.icon-white.icon-ok
|
66
|
+
= t("admin.form.save")
|
67
|
+
%button.btn{type: "submit", name: "_continue"}
|
68
|
+
%i.icon-remove
|
69
|
+
= t("admin.form.cancel")
|
@@ -0,0 +1,3 @@
|
|
1
|
+
See the [community contributed translation section of the README](https://github.com/stephskardal/rails_admin_import#community-contributed-translations) for more languages.
|
2
|
+
|
3
|
+
To contribute a new translation, please put it in a gist and submit a pull request to link your translation in the README.
|
@@ -0,0 +1,50 @@
|
|
1
|
+
|
2
|
+
en:
|
3
|
+
admin:
|
4
|
+
actions:
|
5
|
+
import:
|
6
|
+
title: "Import"
|
7
|
+
menu: "Import"
|
8
|
+
breadcrumb: "Import"
|
9
|
+
link: "Import"
|
10
|
+
bulk_link: "Import"
|
11
|
+
done: "Imported"
|
12
|
+
import:
|
13
|
+
model_fields: "Model fields"
|
14
|
+
association_fields: "Association fields"
|
15
|
+
|
16
|
+
file: "Data file"
|
17
|
+
missing_file: "You must select a file"
|
18
|
+
format: "File format"
|
19
|
+
invalid_format: "Invalid import format."
|
20
|
+
missing_update_lookup: "Your file must contain a column for the 'Update lookup field' you selected."
|
21
|
+
invalid_json: "The JSON data should be an array of records or an object with a key '%{root_key}' set to an array of records"
|
22
|
+
update_if_exists: "Update if exists"
|
23
|
+
update_lookup: "Update lookup field(s)"
|
24
|
+
mapping: "mapping"
|
25
|
+
encoding: "Encoding"
|
26
|
+
legend:
|
27
|
+
fields: "Fields to import"
|
28
|
+
upload: "Upload file"
|
29
|
+
mapping: "Related fields mapping"
|
30
|
+
import_success:
|
31
|
+
create: "Created %{name}"
|
32
|
+
update: "Updated %{name}"
|
33
|
+
import_error:
|
34
|
+
create: "Failed to create %{name}: %{error}"
|
35
|
+
update: "Failed to update %{name}: %{error}"
|
36
|
+
general: "Error during import: %{error}"
|
37
|
+
line_item_limit: "Please limit upload file to %{limit} line items."
|
38
|
+
old_import_hook: >
|
39
|
+
The import hook %{model}.%{method} should take only 1 argument.
|
40
|
+
Data may not imported correctly.
|
41
|
+
See Upgrading section readme in Rails Admin Import.
|
42
|
+
association_not_found: "Association not found. %{error}"
|
43
|
+
help:
|
44
|
+
model_fields: "The fields above may be included in the import file."
|
45
|
+
association_fields: >
|
46
|
+
These fields map to other tables in the database, lookup via attribute selected below.
|
47
|
+
For "many" associations, you may include multiple columns with the same header in the CSV file.
|
48
|
+
update_if_exists: "Update records found with the lookup field below instead of creating new records"
|
49
|
+
file_limit: "Please limit upload file to %{limit} line items."
|
50
|
+
encoding: "Choose file encoding. Leave empty to auto-detect. Ignored for JSON."
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "rails_admin/config/actions"
|
2
|
+
|
3
|
+
module RailsAdmin
|
4
|
+
module Config
|
5
|
+
module Actions
|
6
|
+
class Import < Base
|
7
|
+
RailsAdmin::Config::Actions.register(self)
|
8
|
+
|
9
|
+
register_instance_option :collection do
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
register_instance_option :http_methods do
|
14
|
+
[:get, :post]
|
15
|
+
end
|
16
|
+
|
17
|
+
register_instance_option :controller do
|
18
|
+
proc do
|
19
|
+
@import_model = RailsAdminImport::ImportModel.new(@abstract_model)
|
20
|
+
|
21
|
+
if request.post?
|
22
|
+
format = RailsAdminImport::Formats.from_file(params[:file])
|
23
|
+
record_importer = RailsAdminImport::Formats.for(
|
24
|
+
format, @import_model, params)
|
25
|
+
|
26
|
+
if record_importer.valid?
|
27
|
+
importer = RailsAdminImport::Importer.new(
|
28
|
+
@import_model, params)
|
29
|
+
|
30
|
+
@results = importer.import(record_importer.each)
|
31
|
+
else
|
32
|
+
flash[:error] = record_importer.error
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
render action: @action.template_name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
register_instance_option :link_icon do
|
41
|
+
"icon-folder-open"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module RailsAdminImport
|
2
|
+
module Config
|
3
|
+
class LegacyModel
|
4
|
+
attr_reader :model_name
|
5
|
+
def initialize(model_name)
|
6
|
+
@model_name = model_name
|
7
|
+
end
|
8
|
+
|
9
|
+
def label(_value)
|
10
|
+
# Ignored now
|
11
|
+
# RailsAdmin object_label_method will be used
|
12
|
+
end
|
13
|
+
|
14
|
+
def mapping_key(value)
|
15
|
+
config = RailsAdmin.config(model_name)
|
16
|
+
config.mapping_key(value)
|
17
|
+
end
|
18
|
+
|
19
|
+
def excluded_fields(values)
|
20
|
+
config = RailsAdmin.config(model_name)
|
21
|
+
|
22
|
+
# Call appropriate Rails Admin field list methods
|
23
|
+
config.import do
|
24
|
+
include_all_fields
|
25
|
+
exclude_fields *values
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def extra_fields(values)
|
30
|
+
config = RailsAdmin.config(model_name)
|
31
|
+
|
32
|
+
# Call appropriate Rails Admin field list methods
|
33
|
+
config.import do
|
34
|
+
include_all_fields
|
35
|
+
values.each do |value|
|
36
|
+
field value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "rails_admin/config/sections/base"
|
2
|
+
|
3
|
+
module RailsAdmin
|
4
|
+
module Config
|
5
|
+
module Sections
|
6
|
+
# Configuration of the navigation view
|
7
|
+
class Import < RailsAdmin::Config::Sections::Base
|
8
|
+
register_instance_option(:mapping_key) do
|
9
|
+
:name
|
10
|
+
end
|
11
|
+
|
12
|
+
register_instance_option(:mapping_key_list) do
|
13
|
+
[]
|
14
|
+
end
|
15
|
+
|
16
|
+
register_instance_option(:default_excluded_fields) do
|
17
|
+
[:id, :_id, :created_at, :updated_at, :c_at, :u_at, :deleted_at]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
section = RailsAdmin::Config::Sections::Import
|
25
|
+
name = :import
|
26
|
+
|
27
|
+
# Manually add to Rails Admin as a model configuration section until
|
28
|
+
# there is a better API to do this
|
29
|
+
RailsAdmin::Config::Model.send(:define_method, name) do |&block|
|
30
|
+
@sections = {} unless @sections
|
31
|
+
@sections[name] = section.new(self) unless @sections[name]
|
32
|
+
@sections[name].instance_eval(&block) if block
|
33
|
+
@sections[name]
|
34
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "rails_admin_import/config/legacy_model"
|
2
|
+
|
3
|
+
module RailsAdminImport
|
4
|
+
module Config
|
5
|
+
class << self
|
6
|
+
attr_accessor :logging
|
7
|
+
attr_accessor :line_item_limit
|
8
|
+
attr_accessor :rollback_on_error
|
9
|
+
attr_accessor :update_if_exists
|
10
|
+
attr_accessor :header_converter
|
11
|
+
attr_accessor :csv_options
|
12
|
+
|
13
|
+
# Default is to downcase headers and add underscores to convert into attribute names
|
14
|
+
HEADER_CONVERTER = lambda do |header|
|
15
|
+
# check for nil/blank headers
|
16
|
+
next if header.blank?
|
17
|
+
header.parameterize.underscore
|
18
|
+
end
|
19
|
+
|
20
|
+
def model(model_name, &block)
|
21
|
+
unless @deprecation_shown
|
22
|
+
warn "RailsAdminImport::Config#model is deprecated. " \
|
23
|
+
"Add a import section for your model inside the rails_admin " \
|
24
|
+
"config block. See the Readme.md for more details"
|
25
|
+
@deprecation_shown = true
|
26
|
+
end
|
27
|
+
legacy_config = RailsAdminImport::Config::LegacyModel.new(model_name)
|
28
|
+
legacy_config.instance_eval(&block) if block
|
29
|
+
legacy_config
|
30
|
+
end
|
31
|
+
|
32
|
+
# Reset all configurations to defaults.
|
33
|
+
def reset
|
34
|
+
@logging = false
|
35
|
+
@line_item_limit = 1000
|
36
|
+
@rollback_on_error = false
|
37
|
+
@update_if_exists = false
|
38
|
+
@header_converter = HEADER_CONVERTER
|
39
|
+
@csv_options = {}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Set default values for configuration options on load
|
44
|
+
self.reset
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "csv"
|
2
|
+
|
3
|
+
module RailsAdminImport
|
4
|
+
module Formats
|
5
|
+
class CSVImporter < FileImporter
|
6
|
+
Formats.register(:csv, self)
|
7
|
+
Formats.register(:CSV, self)
|
8
|
+
|
9
|
+
autoload :CharDet, "rchardet"
|
10
|
+
|
11
|
+
def initialize(import_model, params)
|
12
|
+
super
|
13
|
+
@encoding = params[:encoding]
|
14
|
+
@header_converter = RailsAdminImport.config.header_converter
|
15
|
+
end
|
16
|
+
|
17
|
+
# A method that yields a hash of attributes for each record to import
|
18
|
+
def each_record
|
19
|
+
CSV.foreach(filename, csv_options) do |row|
|
20
|
+
attr = convert_to_attributes(row)
|
21
|
+
yield attr unless attr.all? { |field, value| value.blank? }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def csv_options
|
28
|
+
defaults = RailsAdminImport.config.csv_options
|
29
|
+
options = {
|
30
|
+
headers: true,
|
31
|
+
header_converters: @header_converter,
|
32
|
+
encoding: encoding,
|
33
|
+
}
|
34
|
+
|
35
|
+
defaults.merge(options)
|
36
|
+
end
|
37
|
+
|
38
|
+
def encoding
|
39
|
+
from_encoding =
|
40
|
+
if !@encoding.blank?
|
41
|
+
@encoding
|
42
|
+
else
|
43
|
+
detect_encoding
|
44
|
+
end
|
45
|
+
|
46
|
+
to_encoding = import_model.abstract_model.encoding
|
47
|
+
|
48
|
+
if from_encoding && from_encoding != to_encoding
|
49
|
+
"#{from_encoding}:#{to_encoding}"
|
50
|
+
else
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def detect_encoding
|
56
|
+
# Su: Remove CharlockHolmes since it require to another software http://site.icu-project.org/home
|
57
|
+
# which is not available on server
|
58
|
+
# charset = CharlockHolmes::EncodingDetector.detect File.read(filename)
|
59
|
+
# if charset[:confidence] > 0.6
|
60
|
+
# from_encoding = charset[:encoding]
|
61
|
+
# from_encoding = "UTF-8" if from_encoding == "ascii"
|
62
|
+
# end
|
63
|
+
from_encoding = "UTF-8"
|
64
|
+
from_encoding
|
65
|
+
end
|
66
|
+
|
67
|
+
def convert_to_attributes(row)
|
68
|
+
row.each_with_object({}) do |(field, value), record|
|
69
|
+
next if field.blank?
|
70
|
+
field = field.to_sym
|
71
|
+
if import_model.has_multiple_values?(field)
|
72
|
+
field = import_model.pluralize_field(field)
|
73
|
+
(record[field] ||= []) << value
|
74
|
+
else
|
75
|
+
record[field] = value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|