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.
- 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,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module ImporterDsl
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
delegate :allow_revert?, :overridable_columns, to: :class
|
10
|
+
end
|
11
|
+
|
12
|
+
class_methods do
|
13
|
+
def friendly_name(friendly_name = nil)
|
14
|
+
@friendly_name = friendly_name if friendly_name
|
15
|
+
@friendly_name || model || name
|
16
|
+
end
|
17
|
+
|
18
|
+
def introduction(introduction = nil)
|
19
|
+
@introduction ||= []
|
20
|
+
@introduction = introduction if introduction
|
21
|
+
@introduction
|
22
|
+
end
|
23
|
+
|
24
|
+
def columns
|
25
|
+
@columns ||= {}
|
26
|
+
@columns
|
27
|
+
end
|
28
|
+
|
29
|
+
#
|
30
|
+
# Adds a column definition
|
31
|
+
#
|
32
|
+
# @param [Object] args
|
33
|
+
# @param [Object] block which will filter the results before storing the value in the attribute, this is useful for lookups or reformatting
|
34
|
+
def column(*args, &block)
|
35
|
+
options = args.extract_options!
|
36
|
+
|
37
|
+
name = args[0]
|
38
|
+
name ||= options[:name]
|
39
|
+
name ||= options[:attribute]
|
40
|
+
|
41
|
+
hint = args[1]
|
42
|
+
hint ||= options[:hint]
|
43
|
+
|
44
|
+
options[:scope] = self.name.underscore.to_s.tr('/', '.').to_sym
|
45
|
+
|
46
|
+
columns[name] = Importo::ImportColumn.new(name, hint, options[:explanation], options, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
def model(model = nil)
|
50
|
+
@model = model if model
|
51
|
+
@model
|
52
|
+
end
|
53
|
+
|
54
|
+
def allow_duplicates(duplicates)
|
55
|
+
@allow_duplicates = duplicates if duplicates
|
56
|
+
@allow_duplicates
|
57
|
+
end
|
58
|
+
|
59
|
+
def allow_revert(allow)
|
60
|
+
@allow_revert = allow
|
61
|
+
end
|
62
|
+
|
63
|
+
def allow_export(allow)
|
64
|
+
@allow_export = allow
|
65
|
+
end
|
66
|
+
|
67
|
+
def includes_header(includes_header)
|
68
|
+
@includes_header = includes_header if includes_header
|
69
|
+
@includes_header
|
70
|
+
end
|
71
|
+
|
72
|
+
def ignore_header(ignore_header)
|
73
|
+
@ignore_header = ignore_header if ignore_header
|
74
|
+
@ignore_header
|
75
|
+
end
|
76
|
+
|
77
|
+
def csv_options(csv_options = nil)
|
78
|
+
@csv_options = csv_options if csv_options
|
79
|
+
@csv_options
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Set to true to allow duplicate rows to be processed, if false (default) duplicate rows will be marked duplicate and ignored.
|
84
|
+
#
|
85
|
+
def allow_duplicates?
|
86
|
+
@allow_duplicates
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# Set to true when a header is/needs to be present in the file.
|
91
|
+
#
|
92
|
+
def includes_header?
|
93
|
+
@includes_header
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# Allow reverting the import
|
98
|
+
# by default the successfully created records will be destroyed, override this behaviour with the undo method
|
99
|
+
#
|
100
|
+
def allow_revert?
|
101
|
+
@allow_revert
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
# Allow exporting data
|
106
|
+
#
|
107
|
+
def allow_export?
|
108
|
+
@allow_export
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# Set to true when we need to ignore the header for structure check
|
113
|
+
#
|
114
|
+
def ignore_header?
|
115
|
+
@ignore_header
|
116
|
+
end
|
117
|
+
|
118
|
+
def overridable_columns
|
119
|
+
columns.select { |_name, column| column.overridable? }&.map(&:last)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'active_support/concern'
|
5
|
+
|
6
|
+
module Original
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
def original
|
10
|
+
return @original if @original && !@original.is_a?(Hash)
|
11
|
+
|
12
|
+
if import.respond_to?(:attachment_changes) && import.attachment_changes['original']
|
13
|
+
@original ||= import.attachment_changes['original']&.attachable
|
14
|
+
|
15
|
+
if @original.is_a?(Hash)
|
16
|
+
tempfile = Tempfile.new(['ActiveStorage', import.original.filename.extension_with_delimiter])
|
17
|
+
tempfile.binmode
|
18
|
+
tempfile.write(@original[:io].read)
|
19
|
+
@original[:io].rewind
|
20
|
+
tempfile.rewind
|
21
|
+
@original = tempfile
|
22
|
+
end
|
23
|
+
else
|
24
|
+
return unless import&.original&.attachment
|
25
|
+
|
26
|
+
@original = Tempfile.new(['ActiveStorage', import.original.filename.extension_with_delimiter])
|
27
|
+
@original.binmode
|
28
|
+
import.original.download { |block| @original.write(block) }
|
29
|
+
@original.flush
|
30
|
+
@original.rewind
|
31
|
+
end
|
32
|
+
|
33
|
+
@original
|
34
|
+
end
|
35
|
+
|
36
|
+
def structure_valid?
|
37
|
+
return true if !includes_header? || ignore_header?
|
38
|
+
|
39
|
+
invalid_header_names.count.zero?
|
40
|
+
end
|
41
|
+
|
42
|
+
def invalid_header_names
|
43
|
+
invalid_header_names_for_row(header_row)
|
44
|
+
end
|
45
|
+
|
46
|
+
def col_for(translated_name)
|
47
|
+
col = columns.detect { |k, v| v.name == translated_name || k == translated_name }
|
48
|
+
col ||= columns.detect { |k, v| v.allowed_names.include?(translated_name) }
|
49
|
+
col
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def headers_added_by_import
|
55
|
+
%w[import_state import_created_id import_message import_errors].map(&:dup)
|
56
|
+
end
|
57
|
+
|
58
|
+
def cells_from_row(index, clean = true)
|
59
|
+
spreadsheet.row(index).map { |c| clean ? cleaned_data_from_cell(c) : c }
|
60
|
+
end
|
61
|
+
|
62
|
+
def cleaned_data_from_cell(cell)
|
63
|
+
return cell unless cell.respond_to?(:strip)
|
64
|
+
|
65
|
+
strip_tags cell.strip
|
66
|
+
end
|
67
|
+
|
68
|
+
def data_start_row
|
69
|
+
header_row + 1
|
70
|
+
end
|
71
|
+
|
72
|
+
def header_row
|
73
|
+
return 0 unless includes_header?
|
74
|
+
return @header_row if @header_row
|
75
|
+
|
76
|
+
most_valid_counts = (1..20).map do |row_nr|
|
77
|
+
[row_nr, cells_from_row(row_nr).reject(&:nil?).size - invalid_header_names_for_row(row_nr).size]
|
78
|
+
end
|
79
|
+
@header_row = most_valid_counts.max_by(&:last).first
|
80
|
+
end
|
81
|
+
|
82
|
+
def invalid_header_names_for_row(index)
|
83
|
+
stripped_headers = allowed_header_names.map { |name| name.to_s.gsub(/[^A-Za-z]/, '').downcase }
|
84
|
+
cells_from_row(index).reject { |header| stripped_headers.include?(header.to_s.gsub(/[^A-Za-z]/, '').downcase) }
|
85
|
+
end
|
86
|
+
|
87
|
+
def allowed_header_names
|
88
|
+
@allowed_header_names ||= columns.values.map(&:allowed_names).flatten + headers_added_by_import
|
89
|
+
end
|
90
|
+
|
91
|
+
def spreadsheet
|
92
|
+
@spreadsheet ||= case File.extname(original.path)
|
93
|
+
when '.csv' then
|
94
|
+
Roo::CSV.new(original.path, csv_options: csv_options)
|
95
|
+
when '.xls' then
|
96
|
+
Roo::Excel.new(original.path)
|
97
|
+
when '.xlsx' then
|
98
|
+
Roo::Excelx.new(original.path)
|
99
|
+
else
|
100
|
+
raise "Unknown file type: #{original.path.split('/').last}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def duplicate(row_hash, id)
|
105
|
+
Importo::Import.where("results @> '[{\"hash\": \"#{row_hash}\", \"state\": \"success\"}]' AND id <> :id", id: id).first
|
106
|
+
end
|
107
|
+
|
108
|
+
def duplicate?(row_hash, id)
|
109
|
+
return false if allow_duplicates? || row_hash['id'] == id
|
110
|
+
|
111
|
+
duplicate(row_hash, id)
|
112
|
+
end
|
113
|
+
|
114
|
+
def loop_data_rows
|
115
|
+
(data_start_row..spreadsheet.last_row).map do |index|
|
116
|
+
row = cells_from_row(index, false)
|
117
|
+
attributes = Hash[[attribute_names, row].transpose]
|
118
|
+
attributes = attributes.map do |column, value|
|
119
|
+
value = strip_tags(value.strip) if value.respond_to?(:strip) && columns[column]&.options[:strip_tags] != false
|
120
|
+
[column, value]
|
121
|
+
end.to_h
|
122
|
+
attributes.reject! { |k, _v| headers_added_by_import.include?(k) }
|
123
|
+
|
124
|
+
yield attributes, index
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def row_count
|
129
|
+
(spreadsheet.last_row - data_start_row) + 1
|
130
|
+
end
|
131
|
+
|
132
|
+
def nr_to_col(number)
|
133
|
+
('A'..'ZZ').to_a[number]
|
134
|
+
end
|
135
|
+
|
136
|
+
def attribute_names
|
137
|
+
return columns.keys if !includes_header? || ignore_header?
|
138
|
+
|
139
|
+
translated_header_names = cells_from_row(header_row)
|
140
|
+
@header_names = translated_header_names.map do |name|
|
141
|
+
col_for(name)&.first
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def header_names
|
146
|
+
return columns.values.map(&:name) if !includes_header? || ignore_header?
|
147
|
+
|
148
|
+
@header_names ||= cells_from_row(header_row)
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module ResultFeedback
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
|
9
|
+
#
|
10
|
+
# Generates a result excel file as a stream
|
11
|
+
#
|
12
|
+
def results_file
|
13
|
+
xls = Axlsx::Package.new
|
14
|
+
xls.use_shared_strings = true
|
15
|
+
workbook = xls.workbook
|
16
|
+
workbook.styles do |style|
|
17
|
+
alert_cell = style.add_style(bg_color: 'dd7777')
|
18
|
+
duplicate_cell = style.add_style(bg_color: 'ddd777')
|
19
|
+
|
20
|
+
sheet = workbook.add_worksheet(name: I18n.t('importo.sheet.results.name'))
|
21
|
+
|
22
|
+
headers = (header_names - headers_added_by_import) + headers_added_by_import
|
23
|
+
rich_text_headers = headers.map { |header| Axlsx::RichText.new.tap { |rt| rt.add_run(header.dup, b: true) } }
|
24
|
+
sheet.add_row rich_text_headers
|
25
|
+
loop_data_rows do |attributes, index|
|
26
|
+
row_state = result(index, 'state')
|
27
|
+
|
28
|
+
style = case row_state
|
29
|
+
when 'duplicate'
|
30
|
+
duplicate_cell
|
31
|
+
when 'failure'
|
32
|
+
alert_cell
|
33
|
+
end
|
34
|
+
sheet.add_row attributes.values + results(index), style: Array.new(attributes.values.count) + Array.new(headers_added_by_import.count) { style }
|
35
|
+
end
|
36
|
+
|
37
|
+
sheet.auto_filter = "A1:#{sheet.dimension.last_cell_reference}"
|
38
|
+
end
|
39
|
+
|
40
|
+
xls.to_stream
|
41
|
+
end
|
42
|
+
|
43
|
+
def file_name(suffix = nil)
|
44
|
+
base = friendly_name || model.class.name
|
45
|
+
base = base.to_s unless base.is_a?(String)
|
46
|
+
base = base.gsub(/[_\s-]/, '_').pluralize.downcase
|
47
|
+
"#{base}#{suffix.present? ? "_#{suffix}" : '' }.xlsx"
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def register_result(index, details)
|
53
|
+
@import.results ||= []
|
54
|
+
i = @import.results.index { |data| data[:row] == index }
|
55
|
+
if i
|
56
|
+
@import.results[i].merge!(details)
|
57
|
+
else
|
58
|
+
@import.results << details.merge(row: index)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def results(index)
|
63
|
+
[result(index, :state), result(index, :id), result(index, :message), result(index, :errors)]
|
64
|
+
end
|
65
|
+
|
66
|
+
def result(index, field)
|
67
|
+
(@import.results.find { |result| result[:row] == index } || {}).fetch(field, nil)
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'active_support/concern'
|
5
|
+
|
6
|
+
module Revertable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
def revert!
|
10
|
+
undo_all
|
11
|
+
|
12
|
+
import.reverted!
|
13
|
+
rescue StandardError => e
|
14
|
+
import.result_message = "Exception: #{e.message}"
|
15
|
+
Rails.logger.error "Importo exception: #{e.message} backtrace #{e.backtrace.join(';')}"
|
16
|
+
import.failure!
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def undo_all
|
22
|
+
revertable_results = import.results.select { |result| result['state'] == 'success' }
|
23
|
+
|
24
|
+
revertable_results.each do |revertable_result|
|
25
|
+
next unless revertable_result['state'] == 'success'
|
26
|
+
|
27
|
+
begin
|
28
|
+
undo(revertable_result['class'], revertable_result['id'], cells_from_row(revertable_result['row']))
|
29
|
+
revertable_result['state'] = 'reverted'
|
30
|
+
revertable_result.delete('message')
|
31
|
+
revertable_result.delete('errors')
|
32
|
+
rescue StandardError => e
|
33
|
+
result['message'] = "Not reverted: #{e.message}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def undo_row(klass, id, _row)
|
39
|
+
klass.constantize.find(id).destroy
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Importo
|
4
|
+
class BaseImporter
|
5
|
+
include ActionView::Helpers::SanitizeHelper
|
6
|
+
include Importable
|
7
|
+
include Exportable
|
8
|
+
include Revertable
|
9
|
+
include Original
|
10
|
+
include ResultFeedback
|
11
|
+
include ImporterDsl
|
12
|
+
# include ActiveStorage::Downloading
|
13
|
+
|
14
|
+
delegate :friendly_name, :introduction, :model, :columns, :csv_options, :allow_duplicates?, :includes_header?,
|
15
|
+
:ignore_header?, :t, to: :class
|
16
|
+
attr_reader :import, :blob
|
17
|
+
|
18
|
+
def initialize(imprt = nil)
|
19
|
+
@import = imprt
|
20
|
+
I18n.locale = import.locale if import&.locale # Should we do this?? here??
|
21
|
+
end
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def t(key, options = {})
|
25
|
+
if I18n.exists? "importers.#{name.underscore}#{key}".to_sym
|
26
|
+
I18n.t(key,
|
27
|
+
options.merge(scope: "importers.#{name.underscore}".to_sym))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Importo
|
4
|
+
class Import < Importo::ApplicationRecord
|
5
|
+
# include ActiveStorage::Downloading
|
6
|
+
|
7
|
+
belongs_to :importo_ownable, polymorphic: true
|
8
|
+
|
9
|
+
has_many :message_instances, as: :messagable
|
10
|
+
|
11
|
+
validates :kind, presence: true
|
12
|
+
validates :original, presence: true
|
13
|
+
validate :content_validator
|
14
|
+
|
15
|
+
begin
|
16
|
+
has_one_attached :original
|
17
|
+
has_one_attached :result
|
18
|
+
rescue NoMethodError
|
19
|
+
# Weird loading sequence error, is fixed by the lib/importo/helpers
|
20
|
+
end
|
21
|
+
|
22
|
+
state_machine :state, initial: :new do
|
23
|
+
audit_trail class: ResourceStateTransition, as: :resource if "ResourceStateTransition".safe_constantize
|
24
|
+
|
25
|
+
state :importing
|
26
|
+
state :scheduled
|
27
|
+
state :completed
|
28
|
+
state :failed
|
29
|
+
state :reverted
|
30
|
+
|
31
|
+
after_transition any => any do |imprt, transition|
|
32
|
+
CallbackService.perform_later(import: imprt, callback: transition.to_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
after_transition any => :scheduled, do: :schedule_import
|
36
|
+
after_transition any => :reverting, do: :schedule_revert
|
37
|
+
|
38
|
+
event :schedule do
|
39
|
+
transition new: :scheduled
|
40
|
+
end
|
41
|
+
|
42
|
+
event :import do
|
43
|
+
transition new: :importing
|
44
|
+
transition scheduled: :importing
|
45
|
+
transition failed: :importing
|
46
|
+
end
|
47
|
+
|
48
|
+
event :complete do
|
49
|
+
transition importing: :completed
|
50
|
+
end
|
51
|
+
|
52
|
+
event :failure do
|
53
|
+
transition any => :failed
|
54
|
+
end
|
55
|
+
|
56
|
+
event :revert do
|
57
|
+
transition completed: :reverting
|
58
|
+
end
|
59
|
+
|
60
|
+
event :revert do
|
61
|
+
transition reverting: :reverted
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def can_revert?
|
66
|
+
importer.allow_revert? && super
|
67
|
+
end
|
68
|
+
|
69
|
+
def allow_export?
|
70
|
+
importer.class.allow_export?
|
71
|
+
end
|
72
|
+
|
73
|
+
def content_validator
|
74
|
+
errors.add(:original, I18n.t('importo.errors.structure_invalid', invalid_headers: importer.invalid_header_names.join(', '))) unless importer.structure_valid?
|
75
|
+
rescue => e
|
76
|
+
errors.add(:original, I18n.t('importo.errors.parse_error', error: e.message))
|
77
|
+
end
|
78
|
+
|
79
|
+
def importer
|
80
|
+
@importer ||= "#{kind.camelize}Importer".constantize.new(self)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def schedule_import
|
86
|
+
ImportService.perform_later(import: self)
|
87
|
+
end
|
88
|
+
|
89
|
+
def schedule_revert
|
90
|
+
RevertService.perform_later(import: self)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Importo
|
4
|
+
class CallbackService < ApplicationService
|
5
|
+
context do
|
6
|
+
attribute :import
|
7
|
+
attribute :callback
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform
|
11
|
+
Importo.config.import_callback(context.import, context.callback)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Importo
|
4
|
+
class ImportService < ApplicationService
|
5
|
+
def perform
|
6
|
+
sleep 1
|
7
|
+
|
8
|
+
context.import.import!
|
9
|
+
context.import.importer.import!
|
10
|
+
rescue StandardError
|
11
|
+
context.import.failure!
|
12
|
+
context.fail!
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Importo::ImportsTable < ActionTable::ActionTable
|
4
|
+
model Importo::Import
|
5
|
+
|
6
|
+
column(:created_at) { |import| l(import.created_at.in_time_zone(Time.zone), format: :short).to_s }
|
7
|
+
column(:user, filter: { parameter: :ownable, collection_proc: -> { Importo::Import.order(created_at: :desc).limit(200).map(&:importo_ownable).uniq.sort_by(&:name).map { |o| [o.name, "#{o.class.name}##{o.id}"] } } } ) { |import| import.importo_ownable.name }
|
8
|
+
column(:kind, sortable: false)
|
9
|
+
column(:original, sortable: false) { |import| link_to(import.original.filename, main_app.rails_blob_path(import.original, disposition: "attachment"), target: '_blank') }
|
10
|
+
column(:state)
|
11
|
+
column(:result, sortable: false) { |import| import.result.attached? ? link_to(import.result_message, main_app.rails_blob_path(import.result, disposition: "attachment"), target: '_blank') : import.result_message }
|
12
|
+
column(:extra_links, sortable: false) { |import| Importo.config.admin_extra_links(import).map { |name, link| link_to(link[:text], link[:url], title: link[:title], target: '_blank', class: link[:icon]) }}
|
13
|
+
|
14
|
+
column :actions, title: '', sortable: false do |import|
|
15
|
+
content_tag(:span) do
|
16
|
+
if import.can_revert?
|
17
|
+
concat link_to(content_tag(:i, nil, class: 'fa fa-undo'), importo.undo_import_path(import), data: { turbo_method: :post, turbo_confirm: 'Are you sure? This will undo this import.' })
|
18
|
+
end
|
19
|
+
if Importo.config.admin_can_destroy(import)
|
20
|
+
concat link_to(content_tag(:i, nil, class: 'fa fa-trash'), importo.import_path(import), class: 'float-right', data: { turbo_method: :delete, turbo_confirm: 'Are you sure? This will prevent duplicate imports from being detected.' })
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
initial_order :created_at, :desc
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def scope
|
30
|
+
@scope = Importo.config.admin_visible_imports
|
31
|
+
end
|
32
|
+
|
33
|
+
def filtered_scope
|
34
|
+
@filtered_scope = scope
|
35
|
+
@filtered_scope = @filtered_scope.where(importo_ownable_type: params[:ownable].split('#').first, importo_ownable_id: params[:ownable].split('#').last) if params[:ownable]
|
36
|
+
|
37
|
+
@filtered_scope
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
= sts.form_for(@import) do |f|
|
2
|
+
= f.input :kind, as: :hidden
|
3
|
+
= sts.card title: t('.title'), icon: 'fad fa-file-spreadsheet' do |card|
|
4
|
+
- card.action
|
5
|
+
= f.submit
|
6
|
+
|
7
|
+
.grid.grid-cols-12.gap-4
|
8
|
+
.col-span-12
|
9
|
+
.prose
|
10
|
+
p= t('.explanation_html', name: @import.importer.class.friendly_name, sample_path: sample_import_path(kind: @import.kind))
|
11
|
+
- if @import.allow_export?
|
12
|
+
p= t('.export_html', export_path: export_path(kind: @import.kind))
|
13
|
+
|
14
|
+
.col-span-12
|
15
|
+
- @import.importer.overridable_columns.each do |column|
|
16
|
+
= f.fields_for :column_overrides do |fff|
|
17
|
+
- if column.collection
|
18
|
+
= fff.input column.attribute, as: :select, label: column.name, collection: column.collection, include_blank: true, required: false
|
19
|
+
- else
|
20
|
+
= fff.input column.attribute, label: column.name, required: false
|
21
|
+
|
22
|
+
.col-span-12
|
23
|
+
= f.input :original, as: :file
|
24
|
+
|