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,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
|
+
|