importo 3.0.21 → 3.0.23

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -2
  3. data/app/controllers/importo/imports_controller.rb +47 -4
  4. data/app/importers/concerns/importable.rb +15 -7
  5. data/app/importers/concerns/original.rb +5 -2
  6. data/app/importers/importo/import_job_callback.rb +19 -11
  7. data/app/jobs/importo/import_job.rb +18 -28
  8. data/app/jobs/importo/purge_import_job.rb +3 -1
  9. data/app/models/importo/import.rb +10 -6
  10. data/app/services/importo/import_context.rb +3 -11
  11. data/app/services/importo/import_service.rb +4 -3
  12. data/app/views/importo/imports/new.html.slim +3 -1
  13. data/app/views/importo/imports/preview.html.slim +36 -0
  14. data/config/locales/en.yml +12 -1
  15. data/config/routes.rb +7 -4
  16. data/db/migrate/20251117132125_create_good_jobs.rb +40 -0
  17. data/db/migrate/20251117132126_create_good_job_settings.rb +20 -0
  18. data/db/migrate/20251117132127_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb +19 -0
  19. data/db/migrate/20251117132128_create_good_job_batches.rb +35 -0
  20. data/db/migrate/20251117132129_create_good_job_executions.rb +33 -0
  21. data/db/migrate/20251117132130_create_good_jobs_error_event.rb +16 -0
  22. data/db/migrate/20251117132131_recreate_good_job_cron_indexes_with_conditional.rb +45 -0
  23. data/db/migrate/20251117132132_create_good_job_labels.rb +15 -0
  24. data/db/migrate/20251117132133_create_good_job_labels_index.rb +22 -0
  25. data/db/migrate/20251117132134_remove_good_job_active_id_index.rb +21 -0
  26. data/db/migrate/20251117132135_create_index_good_job_jobs_for_candidate_lookup.rb +19 -0
  27. data/db/migrate/20251117132136_create_good_job_execution_error_backtrace.rb +15 -0
  28. data/db/migrate/20251117132137_create_good_job_process_lock_ids.rb +18 -0
  29. data/db/migrate/20251117132138_create_good_job_process_lock_indexes.rb +38 -0
  30. data/db/migrate/20251117132139_create_good_job_execution_duration.rb +15 -0
  31. data/lib/importo/adapters/sidekiq_batch_adapter.rb +79 -0
  32. data/lib/importo/configuration.rb +22 -5
  33. data/lib/importo/engine.rb +3 -0
  34. data/lib/importo/test_helpers.rb +5 -2
  35. data/lib/importo/version.rb +1 -1
  36. metadata +36 -20
  37. data/app/adapters/importo/sidekiq_batch_adapter.rb +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bcca807f9c6f4ca09145b27128b1df2564b226bae7ff922618d399298cf8c231
4
- data.tar.gz: b63912b0490807c5f99dc6928dd69b189cfcd0b92611ac6982d27c45a64b33e4
3
+ metadata.gz: cd095c43d38bb324dea0b158ebe7743bd84630c526b755fc7b031e761fff8e91
4
+ data.tar.gz: 772c031fe4446ce6db81e22c90f57ae9fb1dbfe1cbadd8ee62e0500fd3fe7a4e
5
5
  SHA512:
6
- metadata.gz: 9b01d49b3e56b5823331572c42cccf441325aae495699bc7d52ecb1fb7aaf6468f685ffbfe3412ebc0f64c9d0c34b99f7340f21f420ce655d7804a0aad5beeaa
7
- data.tar.gz: 553dadb41cbe13aae4857265964fd3cf449c641f0bc199938b22db259bc3ce897ea7a34b340f2bb4bf7380647ec5f6a216e4efc0a378d15714bb89f38669e0ce
6
+ metadata.gz: ffc02a4f1a09ad49a624206c615cfcd44a81dd489c28d7c7cdbf6780b451c786e5fde236503585cbf864faaa3e870781de5286b64e10dad21d53d50d1b8e75e5
7
+ data.tar.gz: 75c6b5d26d837b23e5c7ac49514007121346dfb6ef1a7978e80a5105ebe41c666daf986a44c20fe3484bfefddc9d126d0fdad07e379036ccad214a4ca6aec243
data/README.md CHANGED
@@ -92,8 +92,12 @@ Add this line to your application's Gemfile:
92
92
  gem 'importo'
93
93
  ```
94
94
 
95
- Importo depends on Sidekiq Pro's batch functionality,
96
- though you can use [sidekiq-batch](https://github.com/entdec/sidekiq-batch) as a drop-in for that.
95
+ Importo needs Batch functionality, this can either be:
96
+ - Sidekiq Pro's batch functionality,
97
+ - [sidekiq-batch](https://github.com/entdec/sidekiq-batch)
98
+ - GoodJob's Batch
99
+
100
+ Additionally it either needs [Servitium](https://github.com/entdec/servitium) or [Facio](https://github.com/entdec/facio)
97
101
 
98
102
  And then execute:
99
103
 
@@ -8,6 +8,24 @@ module Importo
8
8
  @import = Import.new(kind: params[:kind], locale: I18n.locale)
9
9
  end
10
10
 
11
+ def preview
12
+ @import = Import.find(params[:id])
13
+ if @import&.original&.attachment.present?
14
+ @original = Tempfile.new(['ActiveStorage', @import.original.filename.extension_with_delimiter])
15
+ @original.binmode
16
+ @import.original.download { |block| @original.write(block) }
17
+ @original.flush
18
+ @original.rewind
19
+ sheet = Roo::Excelx.new(@original.path)
20
+ @sheet_data = sheet.parse(headers: true)
21
+ @check_header = @sheet_data.reject{ |h| h.keys == h.values }.map{|h| h.transform_values(&:to_s).compact_blank}.reduce({}, :merge).keys
22
+ end
23
+ if @sheet_data.nil? || @sheet_data.reject{ |h| h.keys == h.values }.blank?
24
+ Signum.error(Current.user, text: t('.no_file'))
25
+ redirect_to action: :new
26
+ end
27
+ end
28
+
11
29
  def create
12
30
  unless import_params
13
31
  @import = Import.new(kind: params[:kind], locale: I18n.locale)
@@ -16,21 +34,46 @@ module Importo
16
34
  return
17
35
  end
18
36
  @import = Import.new(import_params.merge(locale: I18n.locale,
19
- importo_ownable: Importo.config.current_import_owner.call))
20
- if @import.valid? && @import.schedule!
21
- redirect_to importo.new_import_path(params[:kind] || @import.kind)
37
+ importo_ownable: Importo.config.current_import_owner.call))
38
+ if params["commit"] == "upload" && @import.valid? && @import.save!
39
+ @import.confirm!
40
+ @import.schedule!
41
+ redirect_to importo.new_import_path(params[:kind] || @import.kind)
42
+ elsif params["commit"] == "preview" && @import.valid?
43
+ @import.save!
44
+ redirect_to action: :preview, id: @import.id, kind: @import.kind
22
45
  else
23
46
  Signum.error(Current.user, text: t(".flash.error", error: @import.errors&.full_messages&.join(".")))
24
47
  render :new
25
48
  end
26
49
  end
27
50
 
51
+ def cancel
52
+ @import = Import.find(params[:id])
53
+ @import.original.purge if @import.concept?
54
+ Signum.error(Current.user, text: t('.flash.cancel', id: @import.id))
55
+ # flash[:notice] = t('.flash.cancel', id: @import.id)
56
+ redirect_to action: :new, kind: @import.kind
57
+ end
58
+
28
59
  def undo
29
60
  @import = Import.where(importo_ownable: Importo.config.current_import_owner.call).find(params[:id])
30
61
  if @import.can_revert? && @import.revert
31
62
  redirect_to action: :index, notice: "Import reverted"
32
63
  else
33
- redirect_to action: :index, alert: "Import could not be reverted"
64
+ redirect_to action: :index, alert: 'Import could not be reverted'
65
+ end
66
+ end
67
+
68
+ def upload
69
+ @import = Import.find(params[:id])
70
+ @import.checked_columns = params[:selected_items]&.reject { |element| element == "0" }
71
+ @import.confirm! if @import.can_confirm?
72
+ if @import.valid? && @import.schedule!
73
+ redirect_to action: :index
74
+ else
75
+ Signum.error(Current.user, text: t('.flash.error', id: @import.id))
76
+ render :new
34
77
  end
35
78
  end
36
79
 
@@ -123,28 +123,36 @@ module Importable
123
123
  #
124
124
  # Does the actual import
125
125
  #
126
- def import!
127
- raise ArgumentError, "Invalid data structure" unless structure_valid?
126
+ def import!(checked_columns)
127
+ raise ArgumentError, 'Invalid data structure' unless structure_valid?
128
128
 
129
- batch = Importo::SidekiqBatchAdapter.new
129
+ batch = Importo.config.batch_adapter.new
130
130
  batch.description = "#{import.original.filename} - #{import.kind}"
131
131
  batch.properties = {import_id: import.id}
132
- batch.on_success("Importo::ImportJobCallback")
132
+ if Importo.config.batch_adapter == Importo::SidekiqBatchAdapter
133
+ batch.on_success("Importo::ImportJobCallback")
134
+ else
135
+ batch.on_success = "Importo::ImportJobCallback"
136
+ end
133
137
 
134
138
  batch.add do
135
139
  column_with_delay = columns.select { |k, v| v.delay.present? }
136
- loop_data_rows do |attributes, index|
140
+ loop_data_rows(checked_columns) do |attributes, index|
137
141
  if column_with_delay.present?
138
142
  delay = column_with_delay.filter_map do |k, v|
139
143
  next unless attributes[k].present?
140
144
  v.delay.call(attributes[k])
141
145
  end
142
146
  end
143
- Importo::ImportJob.set(wait_until: (delay.max * index).seconds.from_now).perform_async(JSON.dump(attributes), index, import.id) if delay.present?
144
- Importo::ImportJob.perform_async(JSON.dump(attributes), index, import.id) unless delay.present?
147
+ Importo::ImportJob.set(wait_until: (delay.max * index).seconds.from_now).perform_later(JSON.dump(attributes), index, import.id) if delay.present?
148
+ Importo::ImportJob.perform_later(JSON.dump(attributes), index, import.id) unless delay.present?
145
149
  end
146
150
  end
147
151
 
152
+ if defined?(GoodJob::Batch) && Importo.config.batch_adapter == GoodJob::Batch
153
+ batch.enqueue
154
+ end
155
+
148
156
  true
149
157
  rescue => e
150
158
  @import.result_message = "Exception: #{e.message}"
@@ -110,7 +110,7 @@ module Original
110
110
  duplicate(row_hash, id)
111
111
  end
112
112
 
113
- def loop_data_rows
113
+ def loop_data_rows(checked_columns = nil)
114
114
  (data_start_row..spreadsheet.last_row).map do |index|
115
115
  row = cells_from_row(index, false)
116
116
 
@@ -120,7 +120,10 @@ module Original
120
120
  [column, value]
121
121
  end.to_h
122
122
  attributes.reject! { |k, _v| headers_added_by_import.include?(k) }
123
-
123
+ if checked_columns&.dig(:checked_columns).present?
124
+ selected_columns = checked_columns[:checked_columns].map{|i| col_for(i)&.first}
125
+ attributes.reject!{|k, _v| selected_columns.exclude?(k) }
126
+ end
124
127
  yield attributes, index
125
128
  end
126
129
  end
@@ -1,19 +1,20 @@
1
1
  module Importo
2
- class ImportJobCallback
2
+ class ImportJobCallback < ActiveJob::Base
3
+ include Sidekiq::Batch::Callback
3
4
  include Rails.application.routes.url_helpers
4
5
 
5
- def on_complete(_status, options)
6
- options = options.deep_stringify_keys
7
- import = Import.find(options["import_id"])
6
+ def perform(batch, params)
7
+ import = Import.find(batch.properties[:import_id])
8
+ complete_import(import)
9
+ end
10
+
11
+ def complete_import(import)
8
12
  if import.present?
9
13
  results_file = import.importer.results_file
10
- if results_file.is_a?(StringIO)
11
- import.result.attach(io: results_file, filename: import.importer.file_name("results"),
12
- content_type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
13
- else
14
- import.result.attach(io: File.open(results_file), filename: import.importer.file_name("results"),
15
- content_type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
16
- end
14
+ results_file = results_file.is_a?(StringIO) ? results_file : File.open(results_file)
15
+
16
+ import.result.attach(io: results_file, filename: import.importer.file_name("results"),
17
+ content_type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
17
18
 
18
19
  ActiveRecord::Base.uncached do
19
20
  import.result_message = I18n.t("importo.importers.result_message",
@@ -27,5 +28,12 @@ module Importo
27
28
  end
28
29
  end
29
30
  end
31
+
32
+ def on_complete(status, options)
33
+ options = options.deep_stringify_keys
34
+ import = Import.find(options["import_id"])
35
+ complete_import(import)
36
+ end
37
+
30
38
  end
31
39
  end
@@ -1,40 +1,30 @@
1
1
  module Importo
2
- class ImportJob
3
- include Sidekiq::Job
4
- sidekiq_options retry: 5
5
- # queue_as :integration
2
+ class ImportJob < ApplicationJob
3
+ # No options here, gets added from the adapter
6
4
  queue_as Importo.config.queue_name
7
-
8
- sidekiq_retries_exhausted do |msg, _e|
9
- attributes = msg["args"][0]
10
- index = msg["args"][1]
11
- import_id = msg["args"][2]
12
-
13
- execute_row(attributes, index, import_id, true, msg["bid"])
14
- end
15
-
16
- sidekiq_retry_in do |_count, exception, _jobhash|
17
- case exception
18
- when Importo::RetryError
19
- exception.delay
20
- end
21
- end
5
+ include GoodJob::ActiveJobExtensions::Batches
22
6
 
23
7
  def perform(attributes, index, import_id)
24
- self.class.execute_row(attributes, index, import_id, false, bid)
8
+ self.class.execute_row(attributes, index, import_id, false)
25
9
  end
26
10
 
27
- def self.execute_row(attributes, index, import_id, last_attempt, bid)
11
+ def self.execute_row(attributes, index, import_id, last_attempt)
28
12
  attributes = JSON.load(attributes).deep_symbolize_keys if attributes.is_a?(String)
29
13
 
30
14
  import = Import.find(import_id)
31
- record = import.importer.process_data_row(attributes, index, last_attempt: last_attempt)
32
-
33
- batch = Importo::SidekiqBatchAdapter.find(bid)
34
-
35
- if !import.completed? && import.can_complete? && batch.finished?
36
- ImportJobCallback.new.on_complete(batch.status, {import_id: import.id})
37
- end
15
+ import.importer.process_data_row(attributes, index, last_attempt: last_attempt)
16
+
17
+ # Between sidekiq and good job, there's a big difference:
18
+ # - Sidekiq calls on_complete callback when all jobs ran at least once.
19
+ # - GoodJob calls on_complete callback when all jobs are done (including retries).
20
+ # i.e. this logic is only needed for sidekiq
21
+ # if Importo.config.batch_adapter == Importo::SidekiqBatchAdapter
22
+ # batch = Importo::SidekiqBatchAdapter.find(bid)
23
+
24
+ # if !import.completed? && import.can_complete? && batch.finished?
25
+ # ImportJobCallback.new.on_complete("success", {import_id: import.id})
26
+ # end
27
+ # end
38
28
  end
39
29
  end
40
30
  end
@@ -1,7 +1,9 @@
1
1
  module Importo
2
2
  class PurgeImportJob < ApplicationJob
3
- def perform(owner, months)
3
+ def perform(owner, months,state = nil)
4
+
4
5
  imports = Import.where(importo_ownable: owner, created_at: ..months.months.ago.beginning_of_day)
6
+ imports = imports.where(state: state) if state
5
7
 
6
8
  imports.each do |import|
7
9
  import.original.purge
@@ -3,7 +3,7 @@
3
3
  module Importo
4
4
  class Import < Importo::ApplicationRecord
5
5
  # include ActiveStorage::Downloading
6
-
6
+ attr_accessor :checked_columns
7
7
  belongs_to :importo_ownable, polymorphic: true
8
8
 
9
9
  has_many :message_instances, as: :messagable
@@ -12,7 +12,6 @@ module Importo
12
12
  validates :kind, presence: true
13
13
  validates :original, presence: true
14
14
  validate :content_validator
15
-
16
15
  begin
17
16
  has_one_attached :original
18
17
  has_one_attached :result
@@ -20,7 +19,8 @@ module Importo
20
19
  # Weird loading sequence error, is fixed by the lib/importo/helpers
21
20
  end
22
21
 
23
- state_machine :state, initial: :new do
22
+ state_machine :state, initial: :concept do
23
+ state :confirmed
24
24
  state :importing
25
25
  state :scheduled
26
26
  state :completed
@@ -35,11 +35,15 @@ module Importo
35
35
  after_transition any => :reverting, :do => :schedule_revert
36
36
 
37
37
  event :schedule do
38
- transition new: :scheduled
38
+ transition confirmed: :scheduled
39
+ end
40
+
41
+ event :confirm do
42
+ transition concept: :confirmed
39
43
  end
40
44
 
41
45
  event :import do
42
- transition new: :importing
46
+ transition confirmed: :importing
43
47
  transition scheduled: :importing
44
48
  transition failed: :importing
45
49
  end
@@ -111,7 +115,7 @@ module Importo
111
115
  private
112
116
 
113
117
  def schedule_import
114
- ImportScheduleJob.perform_in(5.seconds, id)
118
+ ImportService.perform_later(import: self, checked_columns: self.checked_columns)
115
119
  end
116
120
 
117
121
  def schedule_revert
@@ -1,16 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Importo
4
- if defined?(Servitium) && ApplicationContext < Servitium::Context
5
- class ImportContext < ApplicationContext
6
- input do
7
- attribute :import, type: Import, typecaster: ->(value) { value.is_a?(Import) ? value : Import.find(value) }
8
- end
9
- end
10
- else
11
- class ImportContext < ApplicationContext
12
- attribute :import, :model, class_name: "Importo::Import"
13
- end
14
-
4
+ class ImportContext < ApplicationContext
5
+ attribute :import, type: Import, typecaster: ->(value) { value.is_a?(Import) ? value : Import.find(value) }
6
+ attribute :checked_columns, type: Array, typecaster: ->(value){ value.is_a?(Array) ? value : [] }
15
7
  end
16
8
  end
@@ -4,9 +4,10 @@ module Importo
4
4
  class ImportService < ApplicationService
5
5
  def perform
6
6
  context.import.import!
7
- context.import.importer.import!
8
- rescue
9
- context.import.failure!
7
+ context.import.importer.import!(checked_columns: context.checked_columns )
8
+ rescue StandardError
9
+ context.import.failure!
10
+ context.fail!
10
11
  end
11
12
  end
12
13
  end
@@ -4,7 +4,9 @@
4
4
  = f.input :kind, as: :hidden
5
5
  = sts.card :"importo_imports #{@import.kind}", title: t('.title'), icon: 'fad fa-file-spreadsheet' do |card|
6
6
  - card.with_action
7
- = f.submit
7
+ = f.button 'preview', value: 'preview', class: 'button secondary'
8
+ - card.with_action
9
+ = f.submit nil, value: 'upload'
8
10
 
9
11
  .grid.grid-cols-12.gap-4
10
12
  .col-span-12
@@ -0,0 +1,36 @@
1
+ = sts.form_with(model: false, url: upload_import_path) do |f|
2
+ = sts.card :"importo_imports #{@import.kind}", title: t('.title', kind: @import&.kind.capitalize), icon: 'fad fa-file-spreadsheet' do |card|
3
+ - card.with_action
4
+ / = f.button 'Cancel', value: 'cancel'
5
+ = f.submit 'Upload', value: 'upload'
6
+ = link_to(t('.cancel'), cancel_import_path(@import), method: :post, class: 'button')
7
+ .px-4.sm:px-6.lg:px-8
8
+ / .sm:flex.sm:items-center
9
+ / .sm:flex-auto
10
+ / h1.text-base.font-semibold.leading-6.text-gray-900
11
+ / | Users
12
+ / p.mt-2.text-sm.text-gray-700
13
+ / | A list of all the users in your account including their name, title, email and role.
14
+ / .mt-4.sm:ml-16.sm:mt-0.sm:flex-none
15
+ / button.block.rounded-md.bg-indigo-600.px-3.py-2.text-center.text-sm.font-semibold.text-white.shadow-sm.hover:bg-indigo-500.focus-visible:outline.focus-visible:outline-2.focus-visible:outline-offset-2.focus-visible:outline-indigo-600[type="button"]
16
+ / | Add user
17
+ .mt-8.flow-root
18
+ .-mx-4.-my-2.overflow-x-auto.sm:-mx-6.lg:-mx-8
19
+ .inline-block.min-w-full.py-2.align-middle.bg-white.dark:bg-gray-900.dark:border-gray-700
20
+ table.min-w-full.divide-y.divide-gray-20.border-2
21
+ thead.bg-gray-50
22
+ tr
23
+ - @sheet_data.first.keys.each_with_index do |header, index|
24
+ th.px-3.py-3.5.text-left.text-sm.font-semibold.border-2.text-gray-600[scope="col"]
25
+ = f.check_box :selected_items, { multiple: true, checked: @check_header.include?(header), disabled: @check_header.exclude?(header) }, ActionController::Base.helpers.strip_tags(header)
26
+ tr
27
+ - @sheet_data.first.keys.each_with_index do |header, index|
28
+ th.px-3.py-3.5.text-left.text-sm.font-semibold.border-2.text-gray-600[scope="col"]
29
+ / = f.check_box :active, checked: @check_header.include?(header)
30
+ = ActionController::Base.helpers.strip_tags(header)
31
+ tbody.divide-y.divide-gray-200.bg-white
32
+ - @sheet_data.reject! { |h| h.keys == h.values }&.each do |h|
33
+ tr
34
+ - h.each do |key, value|
35
+ td.whitespace-nowrap.px-3.py-4.text-sm.text-gray-500.border-2
36
+ = value
@@ -2,7 +2,7 @@ en:
2
2
  helpers:
3
3
  submit:
4
4
  importo/import:
5
- create: "Import"
5
+ create: "Upload"
6
6
  importo:
7
7
  sheet:
8
8
  results:
@@ -14,6 +14,8 @@ en:
14
14
  explanation: Purpose
15
15
  example: Example
16
16
  imports:
17
+ cancel:
18
+ success: "Import canceled for id %{id}, Redirecting to new"
17
19
  index:
18
20
  title: Import results
19
21
  card:
@@ -38,6 +40,15 @@ en:
38
40
  no_file: Import failed, please upload a file.
39
41
  error: Import failed, there were problems %{error}.
40
42
  success: "Import scheduled with id %{id}, you will get an email with the results."
43
+ preview:
44
+ no_file: Import failed, please upload valid file.
45
+ title: "%{kind} Import Preview"
46
+ cancel: Cancel
47
+ upload:
48
+ flash:
49
+ no_file: Import failed, please upload a file.
50
+ error: Import failed, there were problems.
51
+ success: "Import scheduled with id %{id}, you will get an email with the results."
41
52
  errors:
42
53
  parse_error: "We encountered a parse error: %{error}"
43
54
  structure_invalid: "The structure is invalid, these are the invalid headers: %{invalid_headers}"
data/config/routes.rb CHANGED
@@ -4,10 +4,13 @@ Importo::Engine.routes.draw do
4
4
  resources :imports, except: %i[new] do
5
5
  member do
6
6
  post :undo
7
+ post :upload
8
+ post :cancel
7
9
  end
8
10
  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"
11
+ get ':kind/new', to: 'imports#new', as: :new_import
12
+ get ':kind/sample', to: 'imports#sample', as: :sample_import
13
+ get ':kind/export', to: 'imports#export', as: :export
14
+ get ':kind/:id/preview', to: 'imports#preview', as: :preview
15
+ root to: 'imports#index'
13
16
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateGoodJobs < ActiveRecord::Migration[7.0]
4
+ def change
5
+ # Uncomment for Postgres v12 or earlier to enable gen_random_uuid() support
6
+ # enable_extension 'pgcrypto'
7
+
8
+ create_table :good_jobs, id: :uuid do |t|
9
+ t.text :queue_name
10
+ t.integer :priority
11
+ t.jsonb :serialized_params
12
+ t.datetime :scheduled_at
13
+ t.datetime :performed_at
14
+ t.datetime :finished_at
15
+ t.text :error
16
+
17
+ t.timestamps
18
+
19
+ t.uuid :active_job_id
20
+ t.text :concurrency_key
21
+ t.text :cron_key
22
+ t.uuid :retried_good_job_id
23
+ t.datetime :cron_at
24
+ end
25
+
26
+ create_table :good_job_processes, id: :uuid do |t|
27
+ t.timestamps
28
+ t.jsonb :state
29
+ end
30
+
31
+ add_index :good_jobs, :scheduled_at, where: "(finished_at IS NULL)", name: :index_good_jobs_on_scheduled_at
32
+ add_index :good_jobs, [:queue_name, :scheduled_at], where: "(finished_at IS NULL)", name: :index_good_jobs_on_queue_name_and_scheduled_at
33
+ add_index :good_jobs, [:active_job_id, :created_at], name: :index_good_jobs_on_active_job_id_and_created_at
34
+ add_index :good_jobs, :concurrency_key, where: "(finished_at IS NULL)", name: :index_good_jobs_on_concurrency_key_when_unfinished
35
+ add_index :good_jobs, [:cron_key, :created_at], name: :index_good_jobs_on_cron_key_and_created_at
36
+ add_index :good_jobs, [:cron_key, :cron_at], name: :index_good_jobs_on_cron_key_and_cron_at, unique: true
37
+ add_index :good_jobs, [:active_job_id], name: :index_good_jobs_on_active_job_id
38
+ add_index :good_jobs, [:finished_at], where: "retried_good_job_id IS NULL AND finished_at IS NOT NULL", name: :index_good_jobs_jobs_on_finished_at
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateGoodJobSettings < ActiveRecord::Migration[7.0]
4
+ def change
5
+ reversible do |dir|
6
+ dir.up do
7
+ # Ensure this incremental update migration is idempotent
8
+ # with monolithic install migration.
9
+ return if connection.table_exists?(:good_job_settings)
10
+ end
11
+ end
12
+
13
+ create_table :good_job_settings, id: :uuid do |t|
14
+ t.timestamps
15
+ t.text :key
16
+ t.jsonb :value
17
+ t.index :key, unique: true
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateIndexGoodJobsJobsOnPriorityCreatedAtWhenUnfinished < ActiveRecord::Migration[7.0]
4
+ disable_ddl_transaction!
5
+
6
+ def change
7
+ reversible do |dir|
8
+ dir.up do
9
+ # Ensure this incremental update migration is idempotent
10
+ # with monolithic install migration.
11
+ return if connection.index_name_exists?(:good_jobs, :index_good_jobs_jobs_on_priority_created_at_when_unfinished)
12
+ end
13
+ end
14
+
15
+ add_index :good_jobs, [:priority, :created_at], order: { priority: "DESC NULLS LAST", created_at: :asc },
16
+ where: "finished_at IS NULL", name: :index_good_jobs_jobs_on_priority_created_at_when_unfinished,
17
+ algorithm: :concurrently
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateGoodJobBatches < ActiveRecord::Migration[7.0]
4
+ def change
5
+ reversible do |dir|
6
+ dir.up do
7
+ # Ensure this incremental update migration is idempotent
8
+ # with monolithic install migration.
9
+ return if connection.table_exists?(:good_job_batches)
10
+ end
11
+ end
12
+
13
+ create_table :good_job_batches, id: :uuid do |t|
14
+ t.timestamps
15
+ t.text :description
16
+ t.jsonb :serialized_properties
17
+ t.text :on_finish
18
+ t.text :on_success
19
+ t.text :on_discard
20
+ t.text :callback_queue_name
21
+ t.integer :callback_priority
22
+ t.datetime :enqueued_at
23
+ t.datetime :discarded_at
24
+ t.datetime :finished_at
25
+ end
26
+
27
+ change_table :good_jobs do |t|
28
+ t.uuid :batch_id
29
+ t.uuid :batch_callback_id
30
+
31
+ t.index :batch_id, where: "batch_id IS NOT NULL"
32
+ t.index :batch_callback_id, where: "batch_callback_id IS NOT NULL"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateGoodJobExecutions < ActiveRecord::Migration[7.0]
4
+ def change
5
+ reversible do |dir|
6
+ dir.up do
7
+ # Ensure this incremental update migration is idempotent
8
+ # with monolithic install migration.
9
+ return if connection.table_exists?(:good_job_executions)
10
+ end
11
+ end
12
+
13
+ create_table :good_job_executions, id: :uuid do |t|
14
+ t.timestamps
15
+
16
+ t.uuid :active_job_id, null: false
17
+ t.text :job_class
18
+ t.text :queue_name
19
+ t.jsonb :serialized_params
20
+ t.datetime :scheduled_at
21
+ t.datetime :finished_at
22
+ t.text :error
23
+
24
+ t.index [:active_job_id, :created_at], name: :index_good_job_executions_on_active_job_id_and_created_at
25
+ end
26
+
27
+ change_table :good_jobs do |t|
28
+ t.boolean :is_discrete
29
+ t.integer :executions_count
30
+ t.text :job_class
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateGoodJobsErrorEvent < ActiveRecord::Migration[7.0]
4
+ def change
5
+ reversible do |dir|
6
+ dir.up do
7
+ # Ensure this incremental update migration is idempotent
8
+ # with monolithic install migration.
9
+ return if connection.column_exists?(:good_jobs, :error_event)
10
+ end
11
+ end
12
+
13
+ add_column :good_jobs, :error_event, :integer, limit: 2
14
+ add_column :good_job_executions, :error_event, :integer, limit: 2
15
+ end
16
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RecreateGoodJobCronIndexesWithConditional < ActiveRecord::Migration[7.0]
4
+ disable_ddl_transaction!
5
+
6
+ def change
7
+ reversible do |dir|
8
+ dir.up do
9
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at_cond)
10
+ add_index :good_jobs, [:cron_key, :created_at], where: "(cron_key IS NOT NULL)",
11
+ name: :index_good_jobs_on_cron_key_and_created_at_cond, algorithm: :concurrently
12
+ end
13
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at_cond)
14
+ add_index :good_jobs, [:cron_key, :cron_at], where: "(cron_key IS NOT NULL)", unique: true,
15
+ name: :index_good_jobs_on_cron_key_and_cron_at_cond, algorithm: :concurrently
16
+ end
17
+
18
+ if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at)
19
+ remove_index :good_jobs, name: :index_good_jobs_on_cron_key_and_created_at
20
+ end
21
+ if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at)
22
+ remove_index :good_jobs, name: :index_good_jobs_on_cron_key_and_cron_at
23
+ end
24
+ end
25
+
26
+ dir.down do
27
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at)
28
+ add_index :good_jobs, [:cron_key, :created_at],
29
+ name: :index_good_jobs_on_cron_key_and_created_at, algorithm: :concurrently
30
+ end
31
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at)
32
+ add_index :good_jobs, [:cron_key, :cron_at], unique: true,
33
+ name: :index_good_jobs_on_cron_key_and_cron_at, algorithm: :concurrently
34
+ end
35
+
36
+ if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_created_at_cond)
37
+ remove_index :good_jobs, name: :index_good_jobs_on_cron_key_and_created_at_cond
38
+ end
39
+ if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_cron_key_and_cron_at_cond)
40
+ remove_index :good_jobs, name: :index_good_jobs_on_cron_key_and_cron_at_cond
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateGoodJobLabels < ActiveRecord::Migration[7.0]
4
+ def change
5
+ reversible do |dir|
6
+ dir.up do
7
+ # Ensure this incremental update migration is idempotent
8
+ # with monolithic install migration.
9
+ return if connection.column_exists?(:good_jobs, :labels)
10
+ end
11
+ end
12
+
13
+ add_column :good_jobs, :labels, :text, array: true
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateGoodJobLabelsIndex < ActiveRecord::Migration[7.0]
4
+ disable_ddl_transaction!
5
+
6
+ def change
7
+ reversible do |dir|
8
+ dir.up do
9
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_labels)
10
+ add_index :good_jobs, :labels, using: :gin, where: "(labels IS NOT NULL)",
11
+ name: :index_good_jobs_on_labels, algorithm: :concurrently
12
+ end
13
+ end
14
+
15
+ dir.down do
16
+ if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_labels)
17
+ remove_index :good_jobs, name: :index_good_jobs_on_labels
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemoveGoodJobActiveIdIndex < ActiveRecord::Migration[7.0]
4
+ disable_ddl_transaction!
5
+
6
+ def change
7
+ reversible do |dir|
8
+ dir.up do
9
+ if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_active_job_id)
10
+ remove_index :good_jobs, name: :index_good_jobs_on_active_job_id
11
+ end
12
+ end
13
+
14
+ dir.down do
15
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_active_job_id)
16
+ add_index :good_jobs, :active_job_id, name: :index_good_jobs_on_active_job_id
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateIndexGoodJobJobsForCandidateLookup < ActiveRecord::Migration[7.0]
4
+ disable_ddl_transaction!
5
+
6
+ def change
7
+ reversible do |dir|
8
+ dir.up do
9
+ # Ensure this incremental update migration is idempotent
10
+ # with monolithic install migration.
11
+ return if connection.index_name_exists?(:good_jobs, :index_good_job_jobs_for_candidate_lookup)
12
+ end
13
+ end
14
+
15
+ add_index :good_jobs, [:priority, :created_at], order: { priority: "ASC NULLS LAST", created_at: :asc },
16
+ where: "finished_at IS NULL", name: :index_good_job_jobs_for_candidate_lookup,
17
+ algorithm: :concurrently
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateGoodJobExecutionErrorBacktrace < ActiveRecord::Migration[7.0]
4
+ def change
5
+ reversible do |dir|
6
+ dir.up do
7
+ # Ensure this incremental update migration is idempotent
8
+ # with monolithic install migration.
9
+ return if connection.column_exists?(:good_job_executions, :error_backtrace)
10
+ end
11
+ end
12
+
13
+ add_column :good_job_executions, :error_backtrace, :text, array: true
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateGoodJobProcessLockIds < ActiveRecord::Migration[7.0]
4
+ def change
5
+ reversible do |dir|
6
+ dir.up do
7
+ # Ensure this incremental update migration is idempotent
8
+ # with monolithic install migration.
9
+ return if connection.column_exists?(:good_jobs, :locked_by_id)
10
+ end
11
+ end
12
+
13
+ add_column :good_jobs, :locked_by_id, :uuid
14
+ add_column :good_jobs, :locked_at, :datetime
15
+ add_column :good_job_executions, :process_id, :uuid
16
+ add_column :good_job_processes, :lock_type, :integer, limit: 2
17
+ end
18
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateGoodJobProcessLockIndexes < ActiveRecord::Migration[7.0]
4
+ disable_ddl_transaction!
5
+
6
+ def change
7
+ reversible do |dir|
8
+ dir.up do
9
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked)
10
+ add_index :good_jobs, [:priority, :scheduled_at],
11
+ order: { priority: "ASC NULLS LAST", scheduled_at: :asc },
12
+ where: "finished_at IS NULL AND locked_by_id IS NULL",
13
+ name: :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked,
14
+ algorithm: :concurrently
15
+ end
16
+
17
+ unless connection.index_name_exists?(:good_jobs, :index_good_jobs_on_locked_by_id)
18
+ add_index :good_jobs, :locked_by_id,
19
+ where: "locked_by_id IS NOT NULL",
20
+ name: :index_good_jobs_on_locked_by_id,
21
+ algorithm: :concurrently
22
+ end
23
+
24
+ unless connection.index_name_exists?(:good_job_executions, :index_good_job_executions_on_process_id_and_created_at)
25
+ add_index :good_job_executions, [:process_id, :created_at],
26
+ name: :index_good_job_executions_on_process_id_and_created_at,
27
+ algorithm: :concurrently
28
+ end
29
+ end
30
+
31
+ dir.down do
32
+ remove_index(:good_jobs, name: :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked) if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_priority_scheduled_at_unfinished_unlocked)
33
+ remove_index(:good_jobs, name: :index_good_jobs_on_locked_by_id) if connection.index_name_exists?(:good_jobs, :index_good_jobs_on_locked_by_id)
34
+ remove_index(:good_job_executions, name: :index_good_job_executions_on_process_id_and_created_at) if connection.index_name_exists?(:good_job_executions, :index_good_job_executions_on_process_id_and_created_at)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateGoodJobExecutionDuration < ActiveRecord::Migration[7.0]
4
+ def change
5
+ reversible do |dir|
6
+ dir.up do
7
+ # Ensure this incremental update migration is idempotent
8
+ # with monolithic install migration.
9
+ return if connection.column_exists?(:good_job_executions, :duration)
10
+ end
11
+ end
12
+
13
+ add_column :good_job_executions, :duration, :interval
14
+ end
15
+ end
@@ -0,0 +1,79 @@
1
+ if !defined?(Sidekiq::Batch)
2
+ module Sidekiq
3
+ class Batch
4
+ def self.new(*)
5
+ raise NotImplementedError, "Sidekiq::Batch is not available. Please install the sidekiq-pro or sidekiq-batch gem."
6
+ end
7
+ end
8
+ end
9
+ end
10
+
11
+ require_relative "../../../app/jobs/importo/import_job"
12
+
13
+ module Importo
14
+ class SidekiqBatchAdapter
15
+ attr_reader :description
16
+ attr_accessor :properties
17
+ attr_writer :instance
18
+
19
+ delegate :description=, to: :@instance
20
+
21
+ def initialize
22
+ @instance = Sidekiq::Batch.new
23
+ end
24
+
25
+ def on_success(job)
26
+ @instance.on(:complete, job.constantize, properties)
27
+ end
28
+
29
+ def add
30
+ @instance.jobs do
31
+ yield
32
+ end
33
+ end
34
+
35
+ def finished?
36
+ status = Sidekiq::Batch::Status.new( @instance.bid)
37
+ status.complete?
38
+ end
39
+
40
+ class << self
41
+ def import_job_base_class
42
+ Object
43
+ end
44
+
45
+ def find(id)
46
+ instance = new
47
+ instance.instance = Sidekiq::Batch.new(id)
48
+ instance
49
+ end
50
+ end
51
+
52
+ module ImportJobIncludes
53
+ extend ActiveSupport::Concern
54
+
55
+ included do
56
+ queue_as Importo.config.queue_name
57
+ sidekiq_options retry: 5
58
+
59
+ sidekiq_retries_exhausted do |msg, _e|
60
+ attributes = msg["args"][0]
61
+ index = msg["args"][1]
62
+ import_id = msg["args"][2]
63
+
64
+ execute_row(attributes, index, import_id, true, msg["bid"])
65
+ end
66
+
67
+ sidekiq_retry_in do |_count, exception, _jobhash|
68
+ case exception
69
+ when Importo::RetryError
70
+ exception.delay
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ # Importo::ImportJob.send(:include, Sidekiq::Job)
79
+ # Importo::ImportJob.send(:include, Importo::SidekiqBatchAdapter::ImportJobIncludes)
@@ -3,9 +3,20 @@
3
3
  module Importo
4
4
  module Options
5
5
  module ClassMethods
6
- def option(name, default: nil)
7
- attr_accessor(name)
8
- schema[name] = default
6
+ def option(name, default: nil, proc: false)
7
+ attr_writer(name)
8
+ schema[name] = {default: default, proc: proc}
9
+
10
+ if schema[name][:proc]
11
+ define_method(name) do |*params|
12
+ value = instance_variable_get(:"@#{name}")
13
+ instance_exec(*params, &value)
14
+ end
15
+ else
16
+ define_method(name) do
17
+ instance_variable_get(:"@#{name}")
18
+ end
19
+ end
9
20
  end
10
21
 
11
22
  def schema
@@ -14,8 +25,8 @@ module Importo
14
25
  end
15
26
 
16
27
  def set_defaults!
17
- self.class.schema.each do |name, default|
18
- instance_variable_set(:"@#{name}", default)
28
+ self.class.schema.each do |name, options|
29
+ instance_variable_set(:"@#{name}", options[:default])
19
30
  end
20
31
  end
21
32
 
@@ -34,6 +45,8 @@ module Importo
34
45
  option :base_service_context, default: "::ApplicationContext"
35
46
  option :current_import_owner, default: lambda {}
36
47
  option :queue_name, default: :import
48
+ # You can either use GoodJob::Batch or Importo::SidekiqBatchAdapter
49
+ option :batch_adapter, default: lambda { GoodJob::Batch }, proc: true
37
50
 
38
51
  option :admin_visible_imports, default: lambda { Importo::Import.where(importo_ownable: Importo.config.current_import_owner) }
39
52
  option(:admin_can_destroy,
@@ -41,6 +54,10 @@ module Importo
41
54
  false
42
55
  end)
43
56
 
57
+ # You can either use GoodJob::Batch or Importo::SidekiqBatchAdapter
58
+ option :batch_adapter, default: lambda { Importo::SidekiqBatchAdapter }, proc: true
59
+ option :import_job_base_class, default: "Object"
60
+
44
61
  # Extra links relevant for this import: { link_name: { icon: 'far fa-..', url: '...' } }
45
62
  option(:admin_extra_links,
46
63
  default: lambda do |import|
@@ -18,6 +18,9 @@ module Importo
18
18
  config.after_initialize do
19
19
  ActiveSupport.on_load(:active_record) do
20
20
  Importo::Import.include(ImportHelpers)
21
+
22
+ # For now put this here to ensure compatibility
23
+ require "importo/adapters/sidekiq_batch_adapter"
21
24
  end
22
25
  end
23
26
  end
@@ -28,9 +28,12 @@ module Importo
28
28
 
29
29
  import.original.attach(io: sheet, filename: filename, content_type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", identify: false)
30
30
  import.save!
31
-
31
+ import.confirm
32
+ import.schedule
32
33
  ImportService.perform(import: import)
33
- ImportJobCallback.new.on_complete("success", {import_id: import.id})
34
+ if Importo.config.batch_adapter == Importo::SidekiqBatchAdapter
35
+ ImportJobCallback.new.on_success(:success,{import_id: import.id})
36
+ end
34
37
  import
35
38
  end
36
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Importo
4
- VERSION = "3.0.21"
4
+ VERSION = "3.0.23"
5
5
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: importo
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.21
4
+ version: 3.0.23
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andre Meij
8
8
  - Tom de Grunt
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-10-02 00:00:00.000000000 Z
12
+ date: 2025-11-24 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: caxlsx
@@ -25,6 +25,20 @@ dependencies:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
27
  version: 3.0.1
28
+ - !ruby/object:Gem::Dependency
29
+ name: csv
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '3.3'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '3.3'
28
42
  - !ruby/object:Gem::Dependency
29
43
  name: rails
30
44
  requirement: !ruby/object:Gem::Requirement
@@ -81,20 +95,6 @@ dependencies:
81
95
  - - "~>"
82
96
  - !ruby/object:Gem::Version
83
97
  version: '2'
84
- - !ruby/object:Gem::Dependency
85
- name: servitium
86
- requirement: !ruby/object:Gem::Requirement
87
- requirements:
88
- - - ">="
89
- - !ruby/object:Gem::Version
90
- version: '1.2'
91
- type: :runtime
92
- prerelease: false
93
- version_requirements: !ruby/object:Gem::Requirement
94
- requirements:
95
- - - ">="
96
- - !ruby/object:Gem::Version
97
- version: '1.2'
98
98
  - !ruby/object:Gem::Dependency
99
99
  name: signum
100
100
  requirement: !ruby/object:Gem::Requirement
@@ -302,7 +302,6 @@ files:
302
302
  - MIT-LICENSE
303
303
  - README.md
304
304
  - Rakefile
305
- - app/adapters/importo/sidekiq_batch_adapter.rb
306
305
  - app/assets/config/importo_manifest.js
307
306
  - app/assets/javascripts/importo/application.js
308
307
  - app/assets/stylesheets/importo/application.css
@@ -337,6 +336,7 @@ files:
337
336
  - app/tables/importo/mensa_imports_table.rb
338
337
  - app/views/importo/imports/index.html.slim
339
338
  - app/views/importo/imports/new.html.slim
339
+ - app/views/importo/imports/preview.html.slim
340
340
  - config/locales/en.yml
341
341
  - config/locales/nl.yml
342
342
  - config/routes.rb
@@ -345,6 +345,21 @@ files:
345
345
  - db/migrate/20190827093548_add_selected_fields_to_import.rb
346
346
  - db/migrate/20230510051447_remove_result_from_imports.rb
347
347
  - db/migrate/20230510083043_create_importo_results.rb
348
+ - db/migrate/20251117132125_create_good_jobs.rb
349
+ - db/migrate/20251117132126_create_good_job_settings.rb
350
+ - db/migrate/20251117132127_create_index_good_jobs_jobs_on_priority_created_at_when_unfinished.rb
351
+ - db/migrate/20251117132128_create_good_job_batches.rb
352
+ - db/migrate/20251117132129_create_good_job_executions.rb
353
+ - db/migrate/20251117132130_create_good_jobs_error_event.rb
354
+ - db/migrate/20251117132131_recreate_good_job_cron_indexes_with_conditional.rb
355
+ - db/migrate/20251117132132_create_good_job_labels.rb
356
+ - db/migrate/20251117132133_create_good_job_labels_index.rb
357
+ - db/migrate/20251117132134_remove_good_job_active_id_index.rb
358
+ - db/migrate/20251117132135_create_index_good_job_jobs_for_candidate_lookup.rb
359
+ - db/migrate/20251117132136_create_good_job_execution_error_backtrace.rb
360
+ - db/migrate/20251117132137_create_good_job_process_lock_ids.rb
361
+ - db/migrate/20251117132138_create_good_job_process_lock_indexes.rb
362
+ - db/migrate/20251117132139_create_good_job_execution_duration.rb
348
363
  - lib/generators/importo/USAGE
349
364
  - lib/generators/importo/importer_generator.rb
350
365
  - lib/generators/importo/install_generator.rb
@@ -357,6 +372,7 @@ files:
357
372
  - lib/generators/satis/templates/config/initializers/satis.rb
358
373
  - lib/importo.rb
359
374
  - lib/importo/acts_as_import_owner.rb
375
+ - lib/importo/adapters/sidekiq_batch_adapter.rb
360
376
  - lib/importo/configuration.rb
361
377
  - lib/importo/engine.rb
362
378
  - lib/importo/import_column.rb
@@ -367,7 +383,7 @@ homepage: https://github.com/entdec/importo
367
383
  licenses:
368
384
  - MIT
369
385
  metadata: {}
370
- post_install_message:
386
+ post_install_message:
371
387
  rdoc_options: []
372
388
  require_paths:
373
389
  - lib
@@ -383,7 +399,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
383
399
  version: '0'
384
400
  requirements: []
385
401
  rubygems_version: 3.4.10
386
- signing_key:
402
+ signing_key:
387
403
  specification_version: 4
388
404
  summary: Rails engine allowing uploads and imports
389
405
  test_files: []
@@ -1,35 +0,0 @@
1
- module Importo
2
- class SidekiqBatchAdapter
3
- attr_reader :description
4
- attr_accessor :properties
5
- attr_writer :instance
6
-
7
- def initialize
8
- @instance = Sidekiq::Batch.new
9
- end
10
-
11
- delegate :description=, :status, to: :@instance
12
-
13
- def on_success(job)
14
- @instance.on(:complete, job.constantize, properties)
15
- end
16
-
17
- def add
18
- @instance.jobs do
19
- yield
20
- end
21
- end
22
-
23
- def finished?
24
- @instance.status.complete?
25
- end
26
-
27
- class << self
28
- def find(id)
29
- instance = new
30
- instance.instance = Sidekiq::Batch.new(id)
31
- instance
32
- end
33
- end
34
- end
35
- end