importo 3.0.26 → 3.0.28

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd6b9f5a7602b4cff37fb084cf00b657358637ceec299afe88895d8b06961cf6
4
- data.tar.gz: edf96c419ecb1504ce5fcd4588ee9f5cd2d121e254c4a39fa283c44edfebb1e8
3
+ metadata.gz: 013fc113e0d5a9cd16cd888462a9d5437ee21d329cbe680ed5a905b6fd53dd23
4
+ data.tar.gz: 2d7b1fadf7a61b4527d02bfc3a206096f0acaa413b26a3cf61ebb43f15a45ef5
5
5
  SHA512:
6
- metadata.gz: 156626804a208b63f94db287102c7c24b24e52022df51a9d339e6f40adea95aee44ade306d2622691a823604ad6cc46d3da9fccb9a5a541be44ac4ab0969d238
7
- data.tar.gz: d34945cd95e615c507670298da6d8bcc2aa5cf92f63bdb69c14d94992d6d89d7e9d8006e3b1a213500580dd13df4efb2b50059b878303d7954b863f0c22d9b4c
6
+ metadata.gz: 967c9f7fc07adadaab9707cd6e6b87098a9672026871992b86d985d73e270bb5269cbd2ae244e0c30e85d89f8bbae4e28bc469e07c8bf3013f0820d8a11f6060
7
+ data.tar.gz: 2c6f5a588b252208692070db52fb65e3a5aa74a42040f9e30acdb8d085725672db888e143b76e28a02ba7b3de3ba8f1b20c33c45af15a52c4b4ea15884ac62a0
@@ -86,15 +86,13 @@ module Importo
86
86
  end
87
87
 
88
88
  def sample
89
- import = Import.new(kind: params[:kind], locale: I18n.locale)
90
- send_data import.importer.sample_file.read,
91
- type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", filename: import.importer.file_name("sample")
89
+ create_export(:sample)
90
+ redirect_back(fallback_location: importo.new_import_path(params[:kind]))
92
91
  end
93
92
 
94
93
  def export
95
- import = Import.new(kind: params[:kind], locale: I18n.locale)
96
- send_data import.importer.export_file.read,
97
- type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", filename: import.importer.file_name("export")
94
+ create_export(:export)
95
+ redirect_back(fallback_location: importo.new_import_path(params[:kind]))
98
96
  end
99
97
 
100
98
  def index
@@ -103,6 +101,13 @@ module Importo
103
101
 
104
102
  private
105
103
 
104
+ def create_export(kind)
105
+ import = Import.new(kind: params[:kind], locale: I18n.locale,
106
+ importo_ownable: Importo.config.current_import_owner.call)
107
+ import.save!(validate: false)
108
+ ExportJob.perform_later(import.id, kind)
109
+ end
110
+
106
111
  def import_params
107
112
  params.require(:import).permit(:original, :kind, :column_overrides,
108
113
  column_overrides: params.dig(:import, :column_overrides)&.keys)
@@ -28,11 +28,6 @@ module Importo
28
28
  I18n.locale = import.locale if import&.locale # Should we do this?? here??
29
29
  end
30
30
 
31
- def state_changed(_import, transition)
32
- run_callbacks(transition.to_name) do
33
- end
34
- end
35
-
36
31
  class << self
37
32
  def t(key, options = {})
38
33
  if I18n.exists? :"importers.#{name.underscore}#{key}"
@@ -10,23 +10,29 @@ module Importo
10
10
  end
11
11
 
12
12
  def complete_import(import)
13
- if import.present?
14
- results_file = import.importer.results_file
15
- results_file = results_file.is_a?(StringIO) ? results_file : File.open(results_file)
16
-
17
- import.result.attach(io: results_file, filename: import.importer.file_name("results"),
18
- content_type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
19
-
20
- ActiveRecord::Base.uncached do
21
- import.result_message = I18n.t("importo.importers.result_message",
22
- nr: import.results.where("details @> ?", {state: "success"}.to_json).count, of: import.importer.send(:row_count))
23
- end
24
-
25
- if import.can_complete?
26
- import.complete!
27
- else
28
- import.save!
29
- end
13
+ return if import.blank?
14
+
15
+ # Sidekiq's batch :complete event can fire as soon as every row has been
16
+ # attempted once, even though some rows failed and are still awaiting a
17
+ # retry. Bail out here so we don't finalize (and report a wrong count)
18
+ # before every row has actually finished processing.
19
+ return unless import.results.count == import.importer.send(:row_count)
20
+
21
+ results_file = import.importer.results_file
22
+ results_file = results_file.is_a?(StringIO) ? results_file : File.open(results_file)
23
+
24
+ import.result.attach(io: results_file, filename: import.importer.file_name("results"),
25
+ content_type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
26
+
27
+ ActiveRecord::Base.uncached do
28
+ import.result_message = I18n.t("importo.importers.result_message",
29
+ nr: import.results.where("details @> ?", {state: "success"}.to_json).count, of: import.importer.send(:row_count))
30
+ end
31
+
32
+ if import.can_complete?
33
+ import.complete!
34
+ else
35
+ import.save!
30
36
  end
31
37
  end
32
38
 
@@ -36,5 +42,9 @@ module Importo
36
42
  import = Import.find(options["import_id"])
37
43
  complete_import(import)
38
44
  end
45
+
46
+ # Sidekiq's batch :success event only fires once every job (including
47
+ # retries) has truly succeeded, so it is the reliable place to finalize.
48
+ alias_method :on_success, :on_complete
39
49
  end
40
50
  end
@@ -0,0 +1,22 @@
1
+ module Importo
2
+ class ExportJob < ApplicationJob
3
+ CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
4
+
5
+ def perform(import_id, kind)
6
+ import = Import.find(import_id)
7
+ import.start_export!
8
+
9
+ importer = import.importer
10
+ file_kind = kind.to_sym
11
+ file = importer.public_send(:"#{file_kind}_file")
12
+
13
+ filename = importer.file_name(file_kind)
14
+ import.result.attach(io: file, filename: filename, content_type: CONTENT_TYPE)
15
+ import.update!(result_message: filename)
16
+ import.complete_export!
17
+ rescue => error
18
+ import&.update!(result_message: "Exception: #{error.message}")
19
+ import&.failure!
20
+ end
21
+ end
22
+ end
@@ -16,13 +16,13 @@ module Importo
16
16
  # - Sidekiq calls on_complete callback when all jobs ran at least once.
17
17
  # - GoodJob calls on_complete callback when all jobs are done (including retries).
18
18
  # i.e. this logic is only needed for sidekiq
19
- return unless Importo.sidekiq?
19
+ # return unless Importo.sidekiq?
20
20
 
21
- batch = Importo::SidekiqBatchAdapter.find(bid)
21
+ # batch = Importo::SidekiqBatchAdapter.find(bid)
22
22
 
23
- if !import.completed? && import.can_complete? && batch.finished?
24
- ImportJobCallback.perform_now(batch, import.id)
25
- end
23
+ # if !import.completed? && import.can_complete? && batch.finished?
24
+ # ImportJobCallback.perform_now(batch, import.id)
25
+ # end
26
26
  end
27
27
  end
28
28
  end
@@ -10,7 +10,7 @@ module Importo
10
10
  has_many :results, class_name: "Importo::Result", dependent: :delete_all
11
11
 
12
12
  validates :kind, presence: true
13
- validates :original, presence: true
13
+ validates :original, presence: true, unless: :export_state?
14
14
  validate :content_validator
15
15
 
16
16
  has_one_attached :original
@@ -23,10 +23,11 @@ module Importo
23
23
  state :completed
24
24
  state :failed
25
25
  state :reverted
26
+ state :exporting
27
+ state :exported
28
+ state :reverting
26
29
 
27
- after_transition any => any do |imprt, transition|
28
- imprt.importer.state_changed(imprt, transition)
29
- end
30
+ around_transition :run_transition_callbacks
30
31
 
31
32
  after_transition any => :scheduled, :do => :schedule_import
32
33
  after_transition any => :reverting, :do => :schedule_revert
@@ -53,6 +54,14 @@ module Importo
53
54
  transition any => :failed
54
55
  end
55
56
 
57
+ event :start_export do
58
+ transition concept: :exporting
59
+ end
60
+
61
+ event :complete_export do
62
+ transition exporting: :exported
63
+ end
64
+
56
65
  event :revert do
57
66
  transition completed: :reverting
58
67
  end
@@ -71,6 +80,8 @@ module Importo
71
80
  end
72
81
 
73
82
  def content_validator
83
+ return if export_state?
84
+
74
85
  unless importer.structure_valid?
75
86
  errors.add(:original,
76
87
  I18n.t("importo.errors.structure_invalid",
@@ -109,6 +120,16 @@ module Importo
109
120
  results.where("details @> ?", '{"state":"processing"}').none?
110
121
  end
111
122
 
123
+ def export_state?
124
+ %w[exporting exported failed].include?(state) && original.blank?
125
+ end
126
+
127
+ def run_transition_callbacks(transition)
128
+ importer.run_callbacks(transition.to_name) do
129
+ yield
130
+ end
131
+ end
132
+
112
133
  private
113
134
 
114
135
  def schedule_import
@@ -30,7 +30,7 @@ if defined? ActionTable
30
30
  private
31
31
 
32
32
  def scope
33
- @scope = Importo.config.admin_visible_imports.call
33
+ @scope = Importo.config.admin_visible_imports.call.where.not(state: %w[exporting exported])
34
34
  @scope
35
35
  end
36
36
  end
@@ -44,7 +44,7 @@ if defined? Mensa
44
44
  private
45
45
 
46
46
  def scope
47
- @scope = Importo.config.admin_visible_imports.call
47
+ @scope = Importo.config.admin_visible_imports.call.where.not(state: %w[exporting exported])
48
48
  @scope = @scope.joins("LEFT JOIN users on importo_imports.importo_ownable_type = 'User' and importo_imports.importo_ownable_id = users.id ")
49
49
  end
50
50
 
@@ -27,8 +27,8 @@ en:
27
27
  new:
28
28
  submit: "Import"
29
29
  title: Import
30
- 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.
31
- 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
30
+ 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}'>sample template</a> with all supported column names and their explanation.
31
+ export_html: You can download the currently stored records.<br>Download the <a href='%{export_path}'>current data</a> with all supported columns
32
32
  error_explanation: "The following problems prohibited this import from completing:"
33
33
  import: Import %{kind}
34
34
  import_button: Import
@@ -18,7 +18,7 @@ nl:
18
18
  new:
19
19
  import:
20
20
  title: Importeer
21
- 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.
21
+ 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}'>voorbeeld file</a> met alle ondersteunde kolomnamen en de bijbehorende uitleg.
22
22
  error_explanation: "De import is mislukt door de volgende problemen:"
23
23
  import: Import %{kind}
24
24
  import_button: Importeer
@@ -21,7 +21,12 @@ module Importo
21
21
  end
22
22
 
23
23
  def on_success(job)
24
+ # Register for both events: :complete can fire prematurely while rows are
25
+ # still awaiting a Sidekiq retry, :success only fires once every row has
26
+ # truly succeeded. ImportJobCallback itself guards against finalizing
27
+ # before every row has actually finished processing.
24
28
  @instance.on(:complete, job.constantize, properties)
29
+ @instance.on(:success, job.constantize, properties)
25
30
  end
26
31
 
27
32
  def add
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Importo
4
- VERSION = "3.0.26"
4
+ VERSION = "3.0.28"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: importo
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.26
4
+ version: 3.0.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andre Meij
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-01-12 00:00:00.000000000 Z
12
+ date: 2026-08-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: caxlsx
@@ -318,6 +318,7 @@ files:
318
318
  - app/importers/importo/base_importer.rb
319
319
  - app/importers/importo/import_job_callback.rb
320
320
  - app/jobs/importo/application_job.rb
321
+ - app/jobs/importo/export_job.rb
321
322
  - app/jobs/importo/import_job.rb
322
323
  - app/jobs/importo/import_revert_job.rb
323
324
  - app/jobs/importo/import_schedule_job.rb