importance 0.2.15 → 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 +4 -4
- data/README.md +38 -0
- data/app/controllers/importance/imports_controller.rb +18 -6
- data/app/models/importance/header.rb +5 -0
- data/app/views/importance/imports/map.html.erb +4 -1
- data/config/locales/de.yml +1 -0
- data/config/locales/en.yml +1 -0
- data/config/locales/fr.yml +1 -0
- data/config/locales/it.yml +1 -0
- data/lib/importance/configuration.rb +41 -4
- data/lib/importance/version.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dbdc483174bf2b9f013661c446a36a6a9c2534413e4f2a284b7ef420521519fe
|
|
4
|
+
data.tar.gz: 68a6102bd591252119a64c75991ac9973b8c13b3553b12ef0b83ef666c29d5d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
49
|
+
redirect_to redirect_target and return
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
@importer.add_spreadsheet(session[:path])
|
|
@@ -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 =
|
|
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
|
|
117
|
+
redirect_to redirect_target, notice: t("importance.success.import_completed")
|
|
115
118
|
end
|
|
116
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|
|
|
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>
|
data/config/locales/de.yml
CHANGED
|
@@ -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.
|
data/config/locales/en.yml
CHANGED
data/config/locales/fr.yml
CHANGED
|
@@ -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é.
|
data/config/locales/it.yml
CHANGED
|
@@ -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?
|
|
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
|
-
|
|
101
|
-
|
|
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
|
data/lib/importance/version.rb
CHANGED
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.
|
|
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:
|
|
11
|
+
date: 2026-09-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -98,7 +98,7 @@ metadata:
|
|
|
98
98
|
homepage_uri: https://github.com/code-fabrik/importance
|
|
99
99
|
source_code_uri: https://github.com/code-fabrik/importance
|
|
100
100
|
changelog_uri: https://github.com/code-fabrik/importance
|
|
101
|
-
post_install_message:
|
|
101
|
+
post_install_message:
|
|
102
102
|
rdoc_options: []
|
|
103
103
|
require_paths:
|
|
104
104
|
- lib
|
|
@@ -113,8 +113,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
113
113
|
- !ruby/object:Gem::Version
|
|
114
114
|
version: '0'
|
|
115
115
|
requirements: []
|
|
116
|
-
rubygems_version: 3.5.
|
|
117
|
-
signing_key:
|
|
116
|
+
rubygems_version: 3.5.16
|
|
117
|
+
signing_key:
|
|
118
118
|
specification_version: 4
|
|
119
119
|
summary: Flexible Excel and CSV import engine with column mapping for Rails applications
|
|
120
120
|
test_files: []
|