importo 2.0.4

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.
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