importance 0.2.14 → 0.3.0

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: 13f0d850ad8abf42c5a29c7afd3e2c80dd9381c85cc06008d1ff3c56b473361f
4
- data.tar.gz: 20c2e53788c462b2041459ea74f1d5d583a7957c6b21a0e64e948c982f1830d4
3
+ metadata.gz: dbdc483174bf2b9f013661c446a36a6a9c2534413e4f2a284b7ef420521519fe
4
+ data.tar.gz: 68a6102bd591252119a64c75991ac9973b8c13b3553b12ef0b83ef666c29d5d3
5
5
  SHA512:
6
- metadata.gz: eb1e8d3b030f659add4ee0ef352457dd22efff8e385e4b2aaa07dd3e6f5619f13e2d5c31b35de4b5356f11fd6fe1588fd2673da88329655ecd1d054b784c900f
7
- data.tar.gz: ca2815baf04a533fa00a81cd9d0b3b22b87dd9e5d2506a12ddf66beba7602913950683cc8f1298e1a760e9883d5055884b288a917da3346cbd908e253768cb66
6
+ metadata.gz: 49455565c8bea02443019f8f5593bfc42d4a5321cacc7ade04ab4a1aeb3e7ace480125c1b5e0c83770478afe5e1aab58e8154cad2783d8f22fcfa186e7712417
7
+ data.tar.gz: 954f6138def8957247b32088f6caa96d938167128965c1cc9d4af1545f220b0c4f8907722468ba7e46d51864f31fb29a71f0a573b325c24a77a35e385c23dc66
data/README.md CHANGED
@@ -120,6 +120,43 @@ Importance.configure do |config|
120
120
  end
121
121
  ```
122
122
 
123
+ ### Repeatable columns (`multiple: true`)
124
+
125
+ Sometimes the number of columns is not known when the importer is written: a file may
126
+ carry one quantity column per location, and the location names live in the header row.
127
+ Declare such an attribute with `multiple: true` and it can be mapped to any number of
128
+ columns at once.
129
+
130
+ ```ruby
131
+ importer.attribute :location, [ "Standort", "Location" ], multiple: true
132
+ ```
133
+
134
+ Instead of a single value, the record then carries a hash of file header => value, in the
135
+ column order of the file:
136
+
137
+ ```ruby
138
+ importer.perform do |records|
139
+ records.each do |record|
140
+ record[:name] # => "MTP-4711"
141
+ record[:location] # => { "WEIS" => 10, "LUBO1" => 10, "LUBO2" => 80, "LOCH" => 0 }
142
+ end
143
+ end
144
+ ```
145
+
146
+ Details worth knowing:
147
+
148
+ - Attributes without `multiple: true` behave exactly as before, and mapping two columns
149
+ to one of them is still rejected with the `duplicate_mapping` error.
150
+ - A `multiple` attribute that is not `optional` requires **at least one** mapped column.
151
+ - Every mapped column appears in the hash, including ones whose cell is empty, so the
152
+ keys are stable across rows. A row is still skipped when *all* of its values are blank.
153
+ - A mapped column with a blank header gets the positional key `column_N` (1-based) rather
154
+ than a blank one.
155
+ - If two columns share the same header, the last one wins. Validate in your `perform`
156
+ block if that matters for your file format.
157
+ - The mapping page does not pre-select columns for `multiple` attributes — the labels are
158
+ only used to name the option in the dropdown, which the user picks per column.
159
+
123
160
  Add a file upload form to your application. You can use libraries like
124
161
  Dropzone.js to create drag and drop interfaces, and you can style them just
125
162
  as you wish. Make sure the path stays, and it is a multipart form.
@@ -202,6 +239,7 @@ en:
202
239
  use_column_as: Use column as
203
240
  ignore: Ignore
204
241
  import: Import
242
+ multiple_label: "%{attribute} (multiple columns)"
205
243
  ```
206
244
 
207
245
  ## Contributing
@@ -11,7 +11,7 @@ module Importance
11
11
 
12
12
  if upload.nil?
13
13
  flash[:alert] = t("importance.errors.no_file")
14
- redirect_to session[:redirect_url] and return
14
+ redirect_to redirect_target and return
15
15
  end
16
16
 
17
17
  upload_extension = File.extname(upload.original_filename).downcase
@@ -19,7 +19,7 @@ module Importance
19
19
 
20
20
  if !supported_extensions.include?(upload_extension)
21
21
  flash[:alert] = t("importance.errors.invalid_file_type", types: supported_extensions.join(", "))
22
- redirect_to session[:redirect_url] and return
22
+ redirect_to redirect_target and return
23
23
  end
24
24
 
25
25
  system_tmp_dir = Dir.tmpdir
@@ -31,7 +31,7 @@ module Importance
31
31
 
32
32
  if !File.exist?(upload_path)
33
33
  flash[:alert] = t("importance.errors.no_file")
34
- redirect_to session[:redirect_url] and return
34
+ redirect_to redirect_target and return
35
35
  end
36
36
 
37
37
  FileUtils.mv(upload_path, persist_path)
@@ -46,7 +46,7 @@ module Importance
46
46
 
47
47
  if @importer.nil?
48
48
  flash[:alert] = t("importance.errors.no_importer")
49
- redirect_to session[:redirect_url] and return
49
+ redirect_to redirect_target and return
50
50
  end
51
51
 
52
52
  @importer.add_spreadsheet(session[:path])
@@ -73,7 +73,7 @@ module Importance
73
73
  @mappings = params[:mappings].permit!.to_h.map { |k, v| [ k.to_i, v ] }.to_h
74
74
 
75
75
  @importer.importer_attributes.each do |attribute|
76
- next if !attribute.options[:required]
76
+ next if attribute.options[:optional]
77
77
  next if @mappings.values.include?(attribute.key.to_s)
78
78
 
79
79
  flash[:alert] = t("importance.errors.missing_mapping", attribute: attribute.labels.first)
@@ -82,9 +82,12 @@ module Importance
82
82
 
83
83
  @mappings.each do |column_index, attribute_name|
84
84
  next if attribute_name == ""
85
+
86
+ attribute = @importer.importer_attributes.find { |attr| attr.key.to_s == attribute_name }
87
+ next if attribute&.options&.dig(:multiple) # multiple attributes absorb any number of columns
85
88
  next if @mappings.values.count(attribute_name) <= 1
86
89
 
87
- attribute_label = @importer.importer_attributes.find { |attr| attr.key.to_s == attribute_name }.labels.first
90
+ attribute_label = attribute&.labels&.first || attribute_name
88
91
  flash[:alert] = t("importance.errors.duplicate_mapping", attribute: attribute_label)
89
92
  render :map, status: :unprocessable_entity and return
90
93
  end
@@ -111,12 +114,21 @@ module Importance
111
114
  if @importer.teardown_callback
112
115
  instance_exec(&@importer.teardown_callback)
113
116
  else
114
- redirect_to (session[:redirect_url] || main_app.root_path), notice: t("importance.success.import_completed")
117
+ redirect_to redirect_target, notice: t("importance.success.import_completed")
115
118
  end
116
- rescue Importance::ImportException => e
119
+ rescue => e
117
120
  if @importer.error_callback
118
121
  instance_exec(e, &@importer.error_callback)
119
122
  end
120
123
  end
124
+
125
+ private
126
+
127
+ # Where to send the user when the import finishes or is rejected. The host app may
128
+ # pass a redirect_url along with the upload; fall back to the application root if it
129
+ # did not, so an error path never redirects to nil.
130
+ def redirect_target
131
+ session[:redirect_url] || main_app.root_path
132
+ end
121
133
  end
122
134
  end
@@ -4,6 +4,11 @@ module Importance
4
4
  attribute_mappings = {}
5
5
 
6
6
  importer_attributes.each do |attribute|
7
+ # A multiple attribute spans an unknown number of columns, and this matcher can
8
+ # claim at most one header per attribute. Pre-selecting a single arbitrary column
9
+ # would be misleading, so leave them for the user to map by hand.
10
+ next if attribute.options&.dig(:multiple)
11
+
7
12
  best_header = nil
8
13
  best_similarity = 0
9
14
 
@@ -14,7 +14,10 @@
14
14
  <%= form.select "mappings[#{file_header_idx}]",
15
15
  options_for_select(
16
16
  [[t('importance.ignore'), ""]] +
17
- @importer.importer_attributes.map { |attr| [attr.labels.first, attr.key] },
17
+ @importer.importer_attributes.map { |attr|
18
+ label = attr.options[:multiple] ? t('importance.multiple_label', attribute: attr.labels.first) : attr.labels.first
19
+ [label, attr.key]
20
+ },
18
21
  default_value
19
22
  ), {}, class: @layout.select_class %>
20
23
  </th>
@@ -3,6 +3,7 @@ de:
3
3
  use_column_as: Spalte verwenden als
4
4
  ignore: Ignorieren
5
5
  import: Importieren
6
+ multiple_label: "%{attribute} (mehrere Spalten)"
6
7
  import_description: Es werden %{count} von %{full_count} Beispieldatensätzen angezeigt.
7
8
  errors:
8
9
  no_importer: Kein Importer ausgewählt.
@@ -3,6 +3,7 @@ en:
3
3
  use_column_as: Use column as
4
4
  ignore: Ignore
5
5
  import: Import
6
+ multiple_label: "%{attribute} (multiple columns)"
6
7
  import_description: Showing %{count} of %{full_count} sample records.
7
8
  errors:
8
9
  no_importer: No importer selected.
@@ -3,6 +3,7 @@ fr:
3
3
  use_column_as: Utiliser la colonne comme
4
4
  ignore: Ignorer
5
5
  import: Importer
6
+ multiple_label: "%{attribute} (plusieurs colonnes)"
6
7
  import_description: Affichage de %{count} sur %{full_count} exemples d'enregistrements.
7
8
  errors:
8
9
  no_importer: Aucun importateur sélectionné.
@@ -3,6 +3,7 @@ it:
3
3
  use_column_as: Utilizzare come
4
4
  ignore: Ignora
5
5
  import: Importare
6
+ multiple_label: "%{attribute} (più colonne)"
6
7
  import_description: Mostrati %{count} di %{full_count} record di esempio.
7
8
  errors:
8
9
  no_importer: Nessun importatore selezionato.
@@ -34,6 +34,12 @@ module Importance
34
34
  end
35
35
 
36
36
  def attribute(key, labels, options = {})
37
+ if options[:multiple] && Array(labels).empty?
38
+ raise ArgumentError, "attribute #{key.inspect} is declared multiple: true but has no labels; " \
39
+ "labels are what the mapping dropdown and error messages display"
40
+ end
41
+
42
+ @multiple_keys = nil
37
43
  @attributes << OpenStruct.new(key: key, labels: labels, options: options)
38
44
  end
39
45
 
@@ -60,10 +66,11 @@ module Importance
60
66
  def add_spreadsheet(path)
61
67
  workbook = Roo::Spreadsheet.open(path, { csv_options: { encoding: "bom|utf-8" } })
62
68
  @worksheet = workbook.sheet(0)
69
+ @file_headers = nil
63
70
  end
64
71
 
65
72
  def file_headers
66
- @worksheet.row(1)
73
+ @file_headers ||= @worksheet.row(1)
67
74
  end
68
75
 
69
76
  def samples
@@ -78,13 +85,22 @@ module Importance
78
85
  @attributes
79
86
  end
80
87
 
88
+ # Keys (as strings) of the attributes declared with multiple: true.
89
+ def multiple_keys
90
+ @multiple_keys ||= @attributes.select { |attr| attr.options[:multiple] }.map { |attr| attr.key.to_s }
91
+ end
92
+
81
93
  # Yields each processed row (a hash of attribute => value) to the given block.
82
94
  # Skips empty rows (all values nil or empty).
83
95
  def each_processed_row(path, mappings)
84
96
  @worksheet.each_with_index do |row, idx|
85
97
  next if idx == 0 # Skip header row
86
98
  record = process_row(row, mappings)
87
- next if record.empty? || record.values.all? { |v| v.nil? || v.to_s.strip.empty? }
99
+ next if record.empty?
100
+ # A multiple attribute holds a hash, whose #to_s is never empty - look at its
101
+ # values instead, or empty trailing rows would be imported.
102
+ values = record.values.flat_map { |v| v.is_a?(Hash) ? v.values : [ v ] }
103
+ next if values.all? { |v| v.nil? || v.to_s.strip.empty? }
88
104
  yield record
89
105
  end
90
106
  end
@@ -92,17 +108,38 @@ module Importance
92
108
  # Turn a row of the form ["Hans", "Robert", 1970, "male", "Apple Inc.", "hr@apple.com"]
93
109
  # and a mapping of the form {"0"=>"first_name", "1"=>"last_name", "2"=>"", "3"=>"", "4"=>"", "5"=>"email"}
94
110
  # into a record of the form { first_name: "Hans", last_name: "Robert", email: "hr@apple.com" }
111
+ #
112
+ # Attributes declared with multiple: true may be mapped to any number of columns and
113
+ # collect a hash of file header => value instead of a single value, e.g.
114
+ # { location: { "WEIS" => 10, "LUBO1" => 80 } }.
95
115
  def process_row(row, mappings)
96
116
  record = {}
97
117
 
98
118
  mappings.each do |column_index, attribute_name|
99
119
  next if attribute_name.nil? || attribute_name == ""
100
- value = row[column_index.to_i]
101
- record[attribute_name.to_sym] = value
120
+
121
+ idx = column_index.to_i
122
+ value = row[idx]
123
+
124
+ if multiple_keys.include?(attribute_name.to_s)
125
+ record[attribute_name.to_sym] ||= {}
126
+ record[attribute_name.to_sym][header_name_for(idx)] = value
127
+ else
128
+ record[attribute_name.to_sym] = value
129
+ end
102
130
  end
103
131
 
104
132
  record
105
133
  end
134
+
135
+ private
136
+
137
+ # The file header of a column, used as the key for multiple attributes. Columns with a
138
+ # blank header fall back to a positional name so they never collide on a nil key.
139
+ def header_name_for(column_index)
140
+ name = file_headers[column_index].to_s.strip
141
+ name.empty? ? "column_#{column_index + 1}" : name
142
+ end
106
143
  end
107
144
 
108
145
  def self.configure
@@ -1,3 +1,3 @@
1
1
  module Importance
2
- VERSION = "0.2.14"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/importance.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  require "importance/version"
2
2
  require "importance/engine"
3
3
  require "importance/configuration"
4
- require "importance/import_exception"
5
4
  require "generators/importance/install/install_generator" if defined?(Rails::Generators)
6
5
 
7
6
  module Importance
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: importance
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.14
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lukas_Skywalker
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-27 00:00:00.000000000 Z
11
+ date: 2026-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -88,7 +88,6 @@ files:
88
88
  - lib/importance.rb
89
89
  - lib/importance/configuration.rb
90
90
  - lib/importance/engine.rb
91
- - lib/importance/import_exception.rb
92
91
  - lib/importance/version.rb
93
92
  - lib/tasks/importance_tasks.rake
94
93
  homepage: https://github.com/code-fabrik/importance
@@ -99,7 +98,7 @@ metadata:
99
98
  homepage_uri: https://github.com/code-fabrik/importance
100
99
  source_code_uri: https://github.com/code-fabrik/importance
101
100
  changelog_uri: https://github.com/code-fabrik/importance
102
- post_install_message:
101
+ post_install_message:
103
102
  rdoc_options: []
104
103
  require_paths:
105
104
  - lib
@@ -114,8 +113,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
113
  - !ruby/object:Gem::Version
115
114
  version: '0'
116
115
  requirements: []
117
- rubygems_version: 3.5.22
118
- signing_key:
116
+ rubygems_version: 3.5.16
117
+ signing_key:
119
118
  specification_version: 4
120
119
  summary: Flexible Excel and CSV import engine with column mapping for Rails applications
121
120
  test_files: []
@@ -1,4 +0,0 @@
1
- module Importance
2
- class ImportException < StandardError
3
- end
4
- end