importo 2.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +99 -0
- data/Rakefile +39 -0
- data/app/assets/config/importo_manifest.js +2 -0
- data/app/assets/javascripts/importo/application.js +13 -0
- data/app/assets/stylesheets/importo/application.css +15 -0
- data/app/controllers/concerns/maintenance_standards.rb +24 -0
- data/app/controllers/importo/application_controller.rb +8 -0
- data/app/controllers/importo/imports_controller.rb +65 -0
- data/app/helpers/importo/application_helper.rb +17 -0
- data/app/importers/concerns/exportable.rb +128 -0
- data/app/importers/concerns/importable.rb +168 -0
- data/app/importers/concerns/importer_dsl.rb +122 -0
- data/app/importers/concerns/original.rb +150 -0
- data/app/importers/concerns/result_feedback.rb +69 -0
- data/app/importers/concerns/revertable.rb +41 -0
- data/app/importers/importo/base_importer.rb +32 -0
- data/app/mailers/importo/application_mailer.rb +8 -0
- data/app/models/importo/application_record.rb +7 -0
- data/app/models/importo/import.rb +93 -0
- data/app/services/importo/application_context.rb +6 -0
- data/app/services/importo/application_service.rb +9 -0
- data/app/services/importo/callback_service.rb +14 -0
- data/app/services/importo/import_context.rb +9 -0
- data/app/services/importo/import_service.rb +15 -0
- data/app/services/importo/revert_service.rb +14 -0
- data/app/tables/importo/imports_table.rb +39 -0
- data/app/views/importo/imports/index.html.slim +2 -0
- data/app/views/importo/imports/new.html.slim +24 -0
- data/config/locales/en.yml +33 -0
- data/config/locales/nl.yml +28 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20180409151031_create_importo_import.rb +21 -0
- data/db/migrate/20180628175535_add_locale_importo_import.rb +7 -0
- data/db/migrate/20190827093548_add_selected_fields_to_import.rb +5 -0
- data/lib/generators/importo/USAGE +8 -0
- data/lib/generators/importo/importer_generator.rb +10 -0
- data/lib/generators/importo/install_generator.rb +27 -0
- data/lib/generators/templates/README +14 -0
- data/lib/generators/templates/application_importer.rb +4 -0
- data/lib/generators/templates/importer.rb +24 -0
- data/lib/generators/templates/importo.rb +21 -0
- data/lib/importo/acts_as_import_owner.rb +11 -0
- data/lib/importo/configuration.rb +68 -0
- data/lib/importo/engine.rb +23 -0
- data/lib/importo/import_column.rb +55 -0
- data/lib/importo/import_helpers.rb +10 -0
- data/lib/importo/version.rb +5 -0
- data/lib/importo.rb +29 -0
- metadata +332 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
en:
|
2
|
+
helpers:
|
3
|
+
submit:
|
4
|
+
importo/import:
|
5
|
+
create: "Import"
|
6
|
+
importo:
|
7
|
+
sheet:
|
8
|
+
results:
|
9
|
+
name: Results
|
10
|
+
explanation:
|
11
|
+
name: Explanation
|
12
|
+
column: Column
|
13
|
+
explanation: Explanation
|
14
|
+
imports:
|
15
|
+
index:
|
16
|
+
title: Import results
|
17
|
+
new:
|
18
|
+
submit: 'Import'
|
19
|
+
title: Import
|
20
|
+
explanation_html: A CSV or Excel file can be used to import records. The first row should be the column names.<br>If an <b>id</b> is supplied it will update the matching record instead of creating a new one.<br>Download a <a href='%{sample_path}' target='_blank'>sample template</a> with all supported column names and their explanation.
|
21
|
+
export_html: You can download the currently stored records.<br>Download the <a href='%{export_path}' target='_blank'>current data</a> with all supported columns
|
22
|
+
error_explanation: 'The following problems prohibited this import from completing:'
|
23
|
+
import: Import %{kind}
|
24
|
+
import_button: Import
|
25
|
+
create:
|
26
|
+
flash:
|
27
|
+
no_file: Import failed, please upload a file.
|
28
|
+
error: Import failed, there were problems.
|
29
|
+
success: 'Import scheduled with id %{id}, you will get an email with the results.'
|
30
|
+
errors:
|
31
|
+
structure_invalid: 'The structure is invalid, these are the invalid headers: %{invalid_headers}'
|
32
|
+
importers:
|
33
|
+
result_message: "Successfully imported %{nr} of %{of} rows"
|
@@ -0,0 +1,28 @@
|
|
1
|
+
nl:
|
2
|
+
importo:
|
3
|
+
sheet:
|
4
|
+
results:
|
5
|
+
name: Resultaten
|
6
|
+
explanation:
|
7
|
+
name: Uitleg
|
8
|
+
column: Kolom
|
9
|
+
explanation: Uitleg
|
10
|
+
imports:
|
11
|
+
index:
|
12
|
+
title: Import resultaten
|
13
|
+
new:
|
14
|
+
import:
|
15
|
+
title: Importeer
|
16
|
+
explanation_html: Een CSV of Excel bestand kan gebruikt worden om records te importeren. De eerste rij moet de kolom namen bevatten.<br>Als een <b>id</b> is ingevuld, zal het bestaande record worden geupdate in plaats van een nieuwe aan te maken.<br>Download een <a href='%{sample_path}' target='_blank'>voorbeeld file</a> met alle ondersteunde kolomnamen en de bijbehorende uitleg.
|
17
|
+
error_explanation: 'De import is mislukt door de volgende problemen:'
|
18
|
+
import: Import %{kind}
|
19
|
+
import_button: Importeer
|
20
|
+
create:
|
21
|
+
flash:
|
22
|
+
no_file: Import mislukt, upload een bestand.
|
23
|
+
error: Import mislukt, er waren problemen, voor details zie hieronder.
|
24
|
+
success: 'Import geplanned met id %{id}, je krijgt een email met de resultaten.'
|
25
|
+
errors:
|
26
|
+
structure_invalid: 'The structuur is ongeldig, dit zijn de ongeldige headers: %{invalid_headers}'
|
27
|
+
importers:
|
28
|
+
result_message: "%{nr} van %{of} rijen succesvol geimporteerd"
|
data/config/routes.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Importo::Engine.routes.draw do
|
4
|
+
resources :imports, except: %i[new] do
|
5
|
+
member do
|
6
|
+
post :undo
|
7
|
+
end
|
8
|
+
end
|
9
|
+
get ':kind/new', to: 'imports#new', as: :new_import
|
10
|
+
get ':kind/sample', to: 'imports#sample', as: :sample_import
|
11
|
+
get ':kind/export', to: 'imports#export', as: :export
|
12
|
+
root to: 'imports#index'
|
13
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class CreateImportoImport < ActiveRecord::Migration[5.1]
|
2
|
+
def change
|
3
|
+
enable_extension 'uuid-ossp'
|
4
|
+
enable_extension 'pgcrypto'
|
5
|
+
|
6
|
+
return if table_exists?(:importo_imports)
|
7
|
+
|
8
|
+
create_table :importo_imports, id: :uuid do |t|
|
9
|
+
t.string :importo_ownable_type, null: false
|
10
|
+
t.uuid :importo_ownable_id, null: false
|
11
|
+
|
12
|
+
t.string :kind
|
13
|
+
t.string :state
|
14
|
+
t.string :file_name
|
15
|
+
t.string :result_message
|
16
|
+
t.jsonb :results
|
17
|
+
|
18
|
+
t.timestamps
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Importo
|
2
|
+
class ImporterGenerator < Rails::Generators::NamedBase
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
|
5
|
+
def copy_initializer_file
|
6
|
+
template "application_importer.rb", "app/importers/application_importer.rb"
|
7
|
+
template "importer.rb", "app/importers/#{file_name}_importer.rb"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/base'
|
4
|
+
|
5
|
+
module Importo
|
6
|
+
module Generators
|
7
|
+
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
9
|
+
source_root File.expand_path('../templates', __dir__)
|
10
|
+
|
11
|
+
desc 'Creates a Importo initializer and copy locale files to your application.'
|
12
|
+
|
13
|
+
def copy_initializer
|
14
|
+
template 'importo.rb', 'config/initializers/importo.rb'
|
15
|
+
end
|
16
|
+
|
17
|
+
def copy_locale
|
18
|
+
copy_file '../../../config/locales/en.yml', 'config/locales/importo.en.yml'
|
19
|
+
copy_file '../../../config/locales/nl.yml', 'config/locales/importo.nl.yml'
|
20
|
+
end
|
21
|
+
|
22
|
+
def show_readme
|
23
|
+
readme 'README' if behavior == :invoke
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
===============================================================================
|
2
|
+
|
3
|
+
Some setup you must do manually if you haven't yet:
|
4
|
+
|
5
|
+
1. Check the initializer (in config/initializers/importo.rb) for additional
|
6
|
+
configuration options. Take not of coupon and provider sections.
|
7
|
+
|
8
|
+
2.
|
9
|
+
|
10
|
+
3.
|
11
|
+
|
12
|
+
4.
|
13
|
+
|
14
|
+
===============================================================================
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class <%=name%>Importer < ApplicationImporter
|
4
|
+
# Whether the excel sheet should contain a header
|
5
|
+
includes_header true
|
6
|
+
# Whether to allow importing of duplicates
|
7
|
+
allow_duplicates false
|
8
|
+
# Whether to ignore the given header and use our internal mapping
|
9
|
+
ignore_header false
|
10
|
+
|
11
|
+
field 'id', 'record ID of the <%=name%> (only if you want to update)'
|
12
|
+
field 'name', 'name of the <%=name%>'
|
13
|
+
|
14
|
+
# Here you will build the record based on the row
|
15
|
+
def build(row)
|
16
|
+
record = <%=name%>.find_or_initialize_by(id: row['id'])
|
17
|
+
record.name = row['name']
|
18
|
+
record
|
19
|
+
end
|
20
|
+
|
21
|
+
# Uncomment if you need to do something before saving the record
|
22
|
+
# def before_save(record, _row)
|
23
|
+
# end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Importo.setup do |config|
|
4
|
+
config.base_controller = '::ApplicationController'
|
5
|
+
config.admin_authentication_module = 'Authenticated'
|
6
|
+
|
7
|
+
# Current import owner
|
8
|
+
config.current_import_owner = -> { User.current }
|
9
|
+
|
10
|
+
# Set callbacks for the import states. You can configure callbacks to work with different states
|
11
|
+
config.import_callbacks = {
|
12
|
+
importing: lambda do |import|
|
13
|
+
end,
|
14
|
+
completed: lambda do |import|
|
15
|
+
end,
|
16
|
+
failed: lambda do |import|
|
17
|
+
end
|
18
|
+
}
|
19
|
+
|
20
|
+
config.queue_name = :import
|
21
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Importo
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :admin_authentication_module
|
6
|
+
attr_accessor :base_controller
|
7
|
+
attr_accessor :base_service
|
8
|
+
attr_accessor :base_service_context
|
9
|
+
attr_accessor :queue_name
|
10
|
+
|
11
|
+
attr_writer :logger
|
12
|
+
attr_writer :current_import_owner
|
13
|
+
attr_writer :import_callbacks
|
14
|
+
attr_writer :admin_visible_imports
|
15
|
+
attr_writer :admin_can_destroy
|
16
|
+
attr_writer :admin_extra_links
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@logger = Logger.new(STDOUT)
|
20
|
+
@logger.level = Logger::INFO
|
21
|
+
@base_controller = '::ApplicationController'
|
22
|
+
@base_service = '::ApplicationService'
|
23
|
+
@base_service_context = '::ApplicationContext'
|
24
|
+
@current_import_owner = -> {}
|
25
|
+
@import_callbacks = {
|
26
|
+
importing: lambda do |_import|
|
27
|
+
end,
|
28
|
+
completed: lambda do |_import|
|
29
|
+
end,
|
30
|
+
failed: lambda do |_import|
|
31
|
+
end
|
32
|
+
}
|
33
|
+
@queue_name = :import
|
34
|
+
|
35
|
+
@admin_visible_imports = -> { Importo::Import.where(importo_ownable: current_import_owner) }
|
36
|
+
@admin_can_destroy = ->(_import) { false }
|
37
|
+
|
38
|
+
# Extra links relevant for this import: { link_name: { icon: 'far fa-..', url: '...' } }
|
39
|
+
@admin_extra_links = ->(_import) { }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Config: logger [Object].
|
43
|
+
def logger
|
44
|
+
@logger.is_a?(Proc) ? instance_exec(&@logger) : @logger
|
45
|
+
end
|
46
|
+
|
47
|
+
def current_import_owner
|
48
|
+
raise 'current_import_owner should be a Proc' unless @current_import_owner.is_a? Proc
|
49
|
+
instance_exec(&@current_import_owner)
|
50
|
+
end
|
51
|
+
|
52
|
+
def import_callback(import, state)
|
53
|
+
instance_exec(import, &@import_callbacks[state]) if @import_callbacks[state]
|
54
|
+
end
|
55
|
+
|
56
|
+
def admin_visible_imports
|
57
|
+
instance_exec(&@admin_visible_imports) if @admin_visible_imports
|
58
|
+
end
|
59
|
+
|
60
|
+
def admin_can_destroy(import)
|
61
|
+
instance_exec(import, &@admin_can_destroy) if @admin_can_destroy
|
62
|
+
end
|
63
|
+
|
64
|
+
def admin_extra_links(import)
|
65
|
+
instance_exec(import, &@admin_extra_links) if @admin_extra_links
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Importo
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace Importo
|
6
|
+
|
7
|
+
initializer 'importo.active_storage.attached' do
|
8
|
+
config.after_initialize do
|
9
|
+
ActiveSupport.on_load(:active_record) do
|
10
|
+
Importo::Import.include(ImportHelpers)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
initializer 'importo.append_migrations' do |app|
|
16
|
+
unless app.root.to_s.match?(root.to_s)
|
17
|
+
config.paths['db/migrate'].expanded.each do |expanded_path|
|
18
|
+
app.config.paths['db/migrate'] << expanded_path
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Importo
|
4
|
+
class ImportColumn
|
5
|
+
attr_accessor :proc, :options
|
6
|
+
attr_writer :name, :hint, :explanation
|
7
|
+
|
8
|
+
def initialize(name, hint, explanation, options, &block)
|
9
|
+
@name = name
|
10
|
+
@hint = hint
|
11
|
+
@explanation = explanation
|
12
|
+
@options = options || {}
|
13
|
+
@proc = block
|
14
|
+
end
|
15
|
+
|
16
|
+
def attribute
|
17
|
+
options[:attribute] || @name
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
name = options[:attribute] || @name
|
22
|
+
I18n.t(".column.#{name}", scope: [:importers, options[:scope]], default: name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def allowed_names
|
26
|
+
return @allowed_names if @allowed_names.present?
|
27
|
+
|
28
|
+
name = options[:attribute] || @name
|
29
|
+
|
30
|
+
@allowed_names = I18n.available_locales.map do |locale|
|
31
|
+
I18n.t(".column.#{name}", scope: [:importers, options[:scope]], locale: locale, default: name)
|
32
|
+
end.compact.uniq
|
33
|
+
end
|
34
|
+
|
35
|
+
def hint
|
36
|
+
I18n.t(".hint.#{options[:attribute]}", scope: [:importers, options[:scope]], default: '') if options[:attribute]
|
37
|
+
end
|
38
|
+
|
39
|
+
def explanation
|
40
|
+
I18n.t(".explanation.#{options[:attribute]}", scope: [:importers, options[:scope]], default: '') if options[:attribute]
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# If set this allows the user to set a value during upload that overrides the uploaded values.
|
45
|
+
def overridable?
|
46
|
+
options[:overridable]
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Collection of values (name, id) that are valid for this field, if a name is entered it will be replaced by the id during pre-processing
|
51
|
+
def collection
|
52
|
+
options[:collection]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/importo.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'axlsx'
|
4
|
+
require 'roo'
|
5
|
+
require 'roo-xls'
|
6
|
+
require 'slim'
|
7
|
+
require 'state_machines-activerecord'
|
8
|
+
# require 'active_storage/downloading'
|
9
|
+
|
10
|
+
require_relative 'importo/engine'
|
11
|
+
require_relative 'importo/acts_as_import_owner'
|
12
|
+
require_relative 'importo/import_column'
|
13
|
+
require_relative 'importo/import_helpers'
|
14
|
+
require_relative 'importo/configuration'
|
15
|
+
|
16
|
+
module Importo
|
17
|
+
class Error < StandardError; end
|
18
|
+
|
19
|
+
class DuplicateRowError < Error; end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
attr_reader :config
|
23
|
+
|
24
|
+
def setup
|
25
|
+
@config = Configuration.new
|
26
|
+
yield config
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|