importo 2.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +99 -0
  4. data/Rakefile +39 -0
  5. data/app/assets/config/importo_manifest.js +2 -0
  6. data/app/assets/javascripts/importo/application.js +13 -0
  7. data/app/assets/stylesheets/importo/application.css +15 -0
  8. data/app/controllers/concerns/maintenance_standards.rb +24 -0
  9. data/app/controllers/importo/application_controller.rb +8 -0
  10. data/app/controllers/importo/imports_controller.rb +65 -0
  11. data/app/helpers/importo/application_helper.rb +17 -0
  12. data/app/importers/concerns/exportable.rb +128 -0
  13. data/app/importers/concerns/importable.rb +168 -0
  14. data/app/importers/concerns/importer_dsl.rb +122 -0
  15. data/app/importers/concerns/original.rb +150 -0
  16. data/app/importers/concerns/result_feedback.rb +69 -0
  17. data/app/importers/concerns/revertable.rb +41 -0
  18. data/app/importers/importo/base_importer.rb +32 -0
  19. data/app/mailers/importo/application_mailer.rb +8 -0
  20. data/app/models/importo/application_record.rb +7 -0
  21. data/app/models/importo/import.rb +93 -0
  22. data/app/services/importo/application_context.rb +6 -0
  23. data/app/services/importo/application_service.rb +9 -0
  24. data/app/services/importo/callback_service.rb +14 -0
  25. data/app/services/importo/import_context.rb +9 -0
  26. data/app/services/importo/import_service.rb +15 -0
  27. data/app/services/importo/revert_service.rb +14 -0
  28. data/app/tables/importo/imports_table.rb +39 -0
  29. data/app/views/importo/imports/index.html.slim +2 -0
  30. data/app/views/importo/imports/new.html.slim +24 -0
  31. data/config/locales/en.yml +33 -0
  32. data/config/locales/nl.yml +28 -0
  33. data/config/routes.rb +13 -0
  34. data/db/migrate/20180409151031_create_importo_import.rb +21 -0
  35. data/db/migrate/20180628175535_add_locale_importo_import.rb +7 -0
  36. data/db/migrate/20190827093548_add_selected_fields_to_import.rb +5 -0
  37. data/lib/generators/importo/USAGE +8 -0
  38. data/lib/generators/importo/importer_generator.rb +10 -0
  39. data/lib/generators/importo/install_generator.rb +27 -0
  40. data/lib/generators/templates/README +14 -0
  41. data/lib/generators/templates/application_importer.rb +4 -0
  42. data/lib/generators/templates/importer.rb +24 -0
  43. data/lib/generators/templates/importo.rb +21 -0
  44. data/lib/importo/acts_as_import_owner.rb +11 -0
  45. data/lib/importo/configuration.rb +68 -0
  46. data/lib/importo/engine.rb +23 -0
  47. data/lib/importo/import_column.rb +55 -0
  48. data/lib/importo/import_helpers.rb +10 -0
  49. data/lib/importo/version.rb +5 -0
  50. data/lib/importo.rb +29 -0
  51. 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,7 @@
1
+ class AddLocaleImportoImport < ActiveRecord::Migration[5.1]
2
+ def change
3
+ return if column_exists?(:importo_imports, :locale)
4
+
5
+ add_column :importo_imports, :locale, :string, default: 'en'
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ class AddSelectedFieldsToImport < ActiveRecord::Migration[5.2]
2
+ def change
3
+ add_column :importo_imports, :column_overrides, :jsonb, default: {}, null: false
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Explain the generator
3
+
4
+ Example:
5
+ rails generate importer Thing
6
+
7
+ This will create:
8
+ app/importers/thing_importer.rb
@@ -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,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationImporter < Importo::BaseImporter
4
+ end
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Importo
4
+ module ActsAsImportOwner
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ has_many :import, as: :importo_ownable, class_name: 'Importo::Import'
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Importo::ImportHelpers
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ has_one_attached :original
8
+ has_one_attached :result
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Importo
4
+ VERSION = '2.0.4'
5
+ 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