importance 0.1.0 → 0.2.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 +185 -5
- data/app/controllers/importance/application_controller.rb +3 -1
- data/app/controllers/importance/imports_controller.rb +134 -0
- data/app/models/importance/blank_layout.rb +19 -0
- data/app/models/importance/bootstrap_layout.rb +19 -0
- data/app/models/importance/header.rb +40 -0
- data/app/views/importance/imports/map.html.erb +82 -0
- data/config/locales/de.yml +5 -0
- data/config/locales/en.yml +5 -0
- data/config/locales/fr.yml +5 -0
- data/config/locales/it.yml +5 -0
- data/config/routes.rb +3 -0
- data/lib/generators/importance/install/USAGE +9 -0
- data/lib/generators/importance/install/install_generator.rb +17 -0
- data/lib/generators/importance/install/templates/importance.rb +68 -0
- data/lib/importance/configuration.rb +67 -0
- data/lib/importance/version.rb +1 -1
- data/lib/importance.rb +20 -1
- metadata +67 -11
- data/app/assets/config/importance_manifest.js +0 -1
- data/app/views/layouts/importance/application.html.erb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e0df06e38c78ddb548f82dd7108a1ff45ac679ad21c8b6549ccba6107cbf37b3
|
4
|
+
data.tar.gz: 1cd461bbaf521ab3d4358faa4accf85553d47fbb9941b8462d7e4342a154b45e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 855023fbe7167ff911683626c1e3f0d2eb0ae058f261f56c7f2e9d96afb01dc40e8a7e192f62fb1f47c79e824a971b8b46dee408d0b68370fc304695a0e66020
|
7
|
+
data.tar.gz: 0b98ee22a71bb61a5cf7a218795eb934b418317dde08ebd22bd267757d7648c531d0ae0e432ec5e4746146ca54562620c2d397c005473fc237aa19cde80f8b23
|
data/README.md
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
# Importance
|
2
|
-
Short description and motivation.
|
3
2
|
|
4
|
-
|
5
|
-
|
3
|
+
Importance allows users to select which columns of an Excel or CSV file should be
|
4
|
+
imported and which ones should be ignored. This makes it possible to upload
|
5
|
+
files with arbitrary headers, as long as all necessary data is contained.
|
6
6
|
|
7
7
|
## Installation
|
8
|
+
|
8
9
|
Add this line to your application's Gemfile:
|
9
10
|
|
10
11
|
```ruby
|
@@ -21,12 +22,191 @@ Or install it yourself as:
|
|
21
22
|
$ gem install importance
|
22
23
|
```
|
23
24
|
|
24
|
-
|
25
|
+
Generate the initializer:
|
26
|
+
|
27
|
+
```bash
|
28
|
+
rails generate importance:install
|
29
|
+
```
|
30
|
+
|
31
|
+
This will create a configuration file at `config/initializers/importance.rb` and mount
|
32
|
+
the engine in `config/routes.rb`.
|
33
|
+
|
34
|
+
## Usage
|
35
|
+
|
36
|
+
Importance allows you to define one or more `importers`, where each allows you
|
37
|
+
to define a different treatment of the data you uploaded.
|
38
|
+
|
39
|
+
Define the uploaders in an initializer, for example `config/initializers/importance.rb`.
|
40
|
+
You can define as many importers as you want.
|
41
|
+
|
42
|
+
Each importer can define callbacks that control what is done before the import, during the import,
|
43
|
+
after the import and if any errors occurred.
|
44
|
+
|
45
|
+
| Callback | Usage |
|
46
|
+
|---|---|
|
47
|
+
| `setup` | Code to be run once before the import. Initialization of an error array, loading of required parent records |
|
48
|
+
| `perform` | The actual import logic. This block receives a collection of `records` for which you write the logic to import. It may be called multiple times if the dataset is large. |
|
49
|
+
| `teardown` | Code to be run one after the import. Cleanup, flushing data to a log. |
|
50
|
+
| `error` | Callback if any unhandled exception occurred. Recives the exception as a parameter. |
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
Importance.configure do |config|
|
54
|
+
config.set_layout :bootstrap
|
55
|
+
|
56
|
+
config.register_importer :students do |importer|
|
57
|
+
importer.attribute :first_name, [ "Vorname", "vorname", "vname", "fname", "l_vorname" ]
|
58
|
+
importer.attribute :last_name, [ "Nachname", "nachname", "nname", "lname", "l_nachname" ]
|
59
|
+
importer.attribute :email, [ "E-Mail", "email", "mail", "l_email" ]
|
60
|
+
|
61
|
+
importer.batch_size 500
|
62
|
+
|
63
|
+
# Setup code runs before import
|
64
|
+
importer.setup do
|
65
|
+
@total_count = 0
|
66
|
+
@errors = []
|
67
|
+
@school = School.find(params[:school_id]) # Access to params
|
68
|
+
end
|
69
|
+
|
70
|
+
# Main import logic has access to instance variables from setup
|
71
|
+
importer.perform do |records|
|
72
|
+
@total_count += records.size
|
73
|
+
|
74
|
+
records.each do |record|
|
75
|
+
begin
|
76
|
+
Student.create(
|
77
|
+
first_name: record[:first_name],
|
78
|
+
last_name: record[:last_name],
|
79
|
+
email: record[:email],
|
80
|
+
created_by: current_user.id, # Access to current_user
|
81
|
+
school_id: @school.id # Access to instance var from setup
|
82
|
+
)
|
83
|
+
rescue => e
|
84
|
+
@errors << { record: record, message: e.message }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Teardown code runs after import
|
90
|
+
importer.teardown do
|
91
|
+
# Can access both controller context and setup variables
|
92
|
+
ActivityLog.create(
|
93
|
+
user: current_user,
|
94
|
+
action: "import",
|
95
|
+
details: "Imported #{@total_count} students with #{@errors.size} errors"
|
96
|
+
)
|
97
|
+
|
98
|
+
# Display errors to the user if any occurred
|
99
|
+
if @errors.any?
|
100
|
+
# Store errors in database to avoid session size limits (4KB)
|
101
|
+
import_log = ImportLog.create!(
|
102
|
+
user: current_user,
|
103
|
+
total_records: @total_count,
|
104
|
+
error_count: @errors.size,
|
105
|
+
errors_data: @errors.to_json
|
106
|
+
)
|
107
|
+
|
108
|
+
flash[:alert] = "Import completed with #{@errors.size} errors. Please review the details below."
|
109
|
+
redirect_to rails_routes.import_log_path(import_log)
|
110
|
+
else
|
111
|
+
redirect_to rails_routes.students_path, notice: "Successfully imported #{@total_count} students."
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Controller code to run after the import
|
116
|
+
importer.error do |exception|
|
117
|
+
redirect_to rails_routes.root_path, alert: "Import failed: #{exception.message}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
```
|
122
|
+
|
123
|
+
Add a file upload form to your application. You can use libraries like
|
124
|
+
Dropzone.js to create drag and drop interfaces, and you can style them just
|
125
|
+
as you wish. Make sure the path stays, and it is a multipart form.
|
25
126
|
|
26
|
-
|
127
|
+
The gem supports Excel files (.xlsx, .xls) and CSV files (.csv). For CSV files,
|
128
|
+
the first row is automatically treated as the header row.
|
129
|
+
|
130
|
+
```erb
|
131
|
+
<%= form_with url: importance.submit_path(importer: :students), multipart: true do |form| %>
|
132
|
+
<%= form.file_field :file, accept: ".xlsx,.xls,.csv" %>
|
133
|
+
<%= form.submit "Submit" %>
|
134
|
+
<% end %>
|
135
|
+
```
|
136
|
+
|
137
|
+
### Displaying Import Errors
|
138
|
+
|
139
|
+
If you collect errors in the `@errors` variable during import (as shown in the example above), you can display them to users in your views. Since sessions have a 4KB limit, errors are stored in the database:
|
140
|
+
|
141
|
+
First, create an ImportLog model to store the errors:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
# app/models/import_log.rb
|
145
|
+
class ImportLog < ApplicationRecord
|
146
|
+
belongs_to :user
|
147
|
+
|
148
|
+
def errors_array
|
149
|
+
JSON.parse(errors_data || '[]')
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
# Migration
|
156
|
+
class CreateImportLogs < ActiveRecord::Migration[7.0]
|
157
|
+
def change
|
158
|
+
create_table :import_logs do |t|
|
159
|
+
t.references :user, null: false, foreign_key: true
|
160
|
+
t.integer :total_records
|
161
|
+
t.integer :error_count
|
162
|
+
t.text :errors_data
|
163
|
+
t.timestamps
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
Then display the errors in your view:
|
170
|
+
|
171
|
+
```erb
|
172
|
+
<!-- In your import_logs/show.html.erb view -->
|
173
|
+
<div class="alert alert-warning">
|
174
|
+
<h4>Import Errors</h4>
|
175
|
+
<p>The following <%= @import_log.error_count %> records could not be imported:</p>
|
176
|
+
|
177
|
+
<% errors = @import_log.errors_array %>
|
178
|
+
<% if errors.size > 50 %>
|
179
|
+
<p><em>Showing first 50 errors (total: <%= errors.size %>)</em></p>
|
180
|
+
<% errors = errors.first(50) %>
|
181
|
+
<% end %>
|
182
|
+
|
183
|
+
<ul>
|
184
|
+
<% errors.each do |error| %>
|
185
|
+
<li>
|
186
|
+
<strong>Row data:</strong> <%= error["record"].inspect %><br>
|
187
|
+
<strong>Error:</strong> <%= error["message"] %>
|
188
|
+
</li>
|
189
|
+
<% end %>
|
190
|
+
</ul>
|
191
|
+
</div>
|
192
|
+
```
|
193
|
+
|
194
|
+
## Customization
|
195
|
+
|
196
|
+
The following translations can be overriden by the application
|
197
|
+
|
198
|
+
```yml
|
199
|
+
en:
|
200
|
+
importance:
|
201
|
+
use_column_as: Use column as
|
202
|
+
ignore: Ignore
|
203
|
+
import: Import
|
204
|
+
```
|
27
205
|
|
28
206
|
## Contributing
|
207
|
+
|
29
208
|
Contribution directions go here.
|
30
209
|
|
31
210
|
## License
|
211
|
+
|
32
212
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require "xsv"
|
2
|
+
require "csv"
|
3
|
+
|
4
|
+
module Importance
|
5
|
+
class ImportsController < ApplicationController
|
6
|
+
# Form submission target. Persist the file and redirect to the mapping page.
|
7
|
+
def submit
|
8
|
+
upload = params[:file]
|
9
|
+
|
10
|
+
raise ArgumentError, "Upload cannot be nil" if upload.nil?
|
11
|
+
|
12
|
+
upload_extension = File.extname(upload.original_filename).downcase
|
13
|
+
supported_extensions = [ ".xlsx", ".xls", ".csv" ]
|
14
|
+
|
15
|
+
raise ArgumentError, "Unsupported file format. Please upload Excel (.xlsx, .xls) or CSV (.csv) files." unless supported_extensions.include?(upload_extension)
|
16
|
+
|
17
|
+
system_tmp_dir = Dir.tmpdir
|
18
|
+
upload_path = upload.tempfile.path
|
19
|
+
persist_filename = "#{SecureRandom.uuid}#{upload_extension}"
|
20
|
+
|
21
|
+
persist_path = File.join(system_tmp_dir, persist_filename)
|
22
|
+
|
23
|
+
raise ArgumentError, "File does not exist at #{upload_path}" if !File.exist?(upload_path)
|
24
|
+
|
25
|
+
FileUtils.mv(upload_path, persist_path)
|
26
|
+
|
27
|
+
session[:path] = persist_path
|
28
|
+
session[:importer] = params[:importer].to_sym
|
29
|
+
|
30
|
+
redirect_to map_path
|
31
|
+
end
|
32
|
+
|
33
|
+
# Mapping page. Load headers and samples, display the form.
|
34
|
+
def map
|
35
|
+
importer = Importance.configuration.importers[session[:importer].to_sym]
|
36
|
+
|
37
|
+
raise ArgumentError, "Importer cannot be nil" if importer.nil?
|
38
|
+
|
39
|
+
if csv_file?
|
40
|
+
csv_data = CSV.read(session[:path], headers: true)
|
41
|
+
@file_headers = csv_data.headers
|
42
|
+
@samples = csv_data.first(5).map(&:to_h)
|
43
|
+
else
|
44
|
+
workbook = Xsv.open(session[:path], parse_headers: true)
|
45
|
+
worksheet = workbook.first
|
46
|
+
@file_headers = worksheet.first.keys
|
47
|
+
@samples = worksheet.first(5)
|
48
|
+
end
|
49
|
+
|
50
|
+
@importer_attributes = importer.attributes
|
51
|
+
@layout = "Importance::#{Importance.configuration.layout.to_s.camelize}Layout".constantize
|
52
|
+
end
|
53
|
+
|
54
|
+
# Import page. Load the file according to the mapping and import it.
|
55
|
+
def import
|
56
|
+
importer = Importance.configuration.importers[session[:importer].to_sym]
|
57
|
+
mappings = params[:mappings]
|
58
|
+
|
59
|
+
raise ArgumentError, "Mapping cannot be nil" if mappings.nil?
|
60
|
+
|
61
|
+
if importer.setup_callback
|
62
|
+
instance_exec(&importer.setup_callback)
|
63
|
+
end
|
64
|
+
|
65
|
+
begin
|
66
|
+
records_to_import = []
|
67
|
+
|
68
|
+
each_processed_row(mappings) do |record|
|
69
|
+
records_to_import << record
|
70
|
+
|
71
|
+
if importer.batch && records_to_import.size >= importer.batch
|
72
|
+
instance_exec(records_to_import, &importer.perform_callback)
|
73
|
+
records_to_import = []
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
if records_to_import.any?
|
78
|
+
instance_exec(records_to_import, &importer.perform_callback)
|
79
|
+
end
|
80
|
+
|
81
|
+
if importer.teardown_callback
|
82
|
+
instance_exec(&importer.teardown_callback)
|
83
|
+
else
|
84
|
+
redirect_to session[:redirect_url] || root_path, notice: "Import completed."
|
85
|
+
end
|
86
|
+
|
87
|
+
rescue => e
|
88
|
+
if importer.error_callback
|
89
|
+
instance_exec(e, &importer.error_callback)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def csv_file?
|
97
|
+
File.extname(session[:path]).downcase == ".csv"
|
98
|
+
end
|
99
|
+
|
100
|
+
def each_processed_row(mappings)
|
101
|
+
if csv_file?
|
102
|
+
CSV.foreach(session[:path], headers: true) do |row|
|
103
|
+
record = process_row(row.to_h, mappings)
|
104
|
+
next if record.empty? || record.values.all? { |v| v.nil? || v.to_s.strip.empty? }
|
105
|
+
yield record
|
106
|
+
end
|
107
|
+
else
|
108
|
+
workbook = Xsv.open(session[:path], parse_headers: true)
|
109
|
+
worksheet = workbook.first
|
110
|
+
worksheet.each do |row|
|
111
|
+
record = process_row(row, mappings)
|
112
|
+
next if record.empty? || record.values.all? { |v| v.nil? || v.to_s.strip.empty? }
|
113
|
+
yield record
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def process_row(row, mappings)
|
119
|
+
record = {}
|
120
|
+
row.each do |row_header, value|
|
121
|
+
attribute = mappings.permit!.to_h.find { |column_name, attribute_name| column_name == row_header }
|
122
|
+
next if attribute.nil?
|
123
|
+
attribute = attribute[1]
|
124
|
+
next if attribute.nil? || attribute == ""
|
125
|
+
record[attribute.to_sym] = value
|
126
|
+
end
|
127
|
+
record
|
128
|
+
end
|
129
|
+
|
130
|
+
def rails_routes
|
131
|
+
::Rails.application.routes.url_helpers
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Importance
|
2
|
+
class BootstrapLayout < BlankLayout
|
3
|
+
def self.select_class
|
4
|
+
"form-select"
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.submit_class
|
8
|
+
"btn btn-primary"
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.table_class
|
12
|
+
"table"
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.wrapper_class
|
16
|
+
"table-responsive"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Importance
|
2
|
+
class Header
|
3
|
+
def self.match_attributes_to_headers(importer_attributes, file_headers)
|
4
|
+
attribute_mappings = {}
|
5
|
+
|
6
|
+
importer_attributes.each do |attribute|
|
7
|
+
best_header = nil
|
8
|
+
best_similarity = 0
|
9
|
+
|
10
|
+
file_headers.each do |header|
|
11
|
+
attribute.labels.each do |label|
|
12
|
+
if header == label
|
13
|
+
best_header = header
|
14
|
+
best_similarity = 1.0
|
15
|
+
break # No need to check further if an exact match is found
|
16
|
+
end
|
17
|
+
distance = DidYouMean::Levenshtein.distance(header, label)
|
18
|
+
percentage = distance / header.length.to_f
|
19
|
+
similarity = 1 - percentage
|
20
|
+
if similarity > best_similarity
|
21
|
+
best_similarity = similarity
|
22
|
+
best_header = header
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Only assign if similarity is reasonable (> 0.5) and header isn't already taken
|
28
|
+
if best_similarity > 0.5 && !attribute_mappings.values.include?(best_header)
|
29
|
+
attribute_mappings[attribute.key] = best_header
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
attribute_mappings
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.default_value_for_header(file_header, attribute_mappings)
|
37
|
+
attribute_mappings.key(file_header) || ""
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
<%= form_with url: importance.import_path, method: :post do |form| %>
|
2
|
+
<div class="importance-wrapper <%= @layout.wrapper_class %>">
|
3
|
+
<%= form.submit t('importance.import'), class: @layout.submit_class %>
|
4
|
+
<table class="importance-table <%= @layout.table_class %>">
|
5
|
+
<thead>
|
6
|
+
<tr>
|
7
|
+
<% @file_headers.each do |file_header| %>
|
8
|
+
<th>
|
9
|
+
<%= t('importance.use_column_as') %>
|
10
|
+
<%
|
11
|
+
attribute_mappings = Importance::Header.match_attributes_to_headers(@importer_attributes, @file_headers)
|
12
|
+
default_value = Importance::Header.default_value_for_header(file_header, attribute_mappings)
|
13
|
+
%>
|
14
|
+
<%= form.select "mappings[#{file_header}]",
|
15
|
+
options_for_select(
|
16
|
+
[[t('importance.ignore'), ""]] +
|
17
|
+
@importer_attributes.map { |attr| [attr.labels.first, attr.key] },
|
18
|
+
default_value
|
19
|
+
), {}, class: @layout.select_class %>
|
20
|
+
</th>
|
21
|
+
<% end %>
|
22
|
+
</tr>
|
23
|
+
<tr>
|
24
|
+
<% @file_headers.each do |file_header| %>
|
25
|
+
<th><%= file_header %></th>
|
26
|
+
<% end %>
|
27
|
+
</tr>
|
28
|
+
</thead>
|
29
|
+
<tbody>
|
30
|
+
<% @samples.each do |sample| %>
|
31
|
+
<tr>
|
32
|
+
<% @file_headers.each do |file_header| %>
|
33
|
+
<td><%= sample[file_header] %></td>
|
34
|
+
<% end %>
|
35
|
+
</tr>
|
36
|
+
<% end %>
|
37
|
+
</tbody>
|
38
|
+
</table>
|
39
|
+
</div>
|
40
|
+
<% end %>
|
41
|
+
|
42
|
+
<script>
|
43
|
+
const table = document.querySelector('.importance-table');
|
44
|
+
if (table) {
|
45
|
+
const selects = table.querySelectorAll('thead tr:first-child th select');
|
46
|
+
const dataRows = table.querySelectorAll('tbody tr, thead tr');
|
47
|
+
|
48
|
+
const updateIgnoredColumns = () => {
|
49
|
+
dataRows.forEach(row => {
|
50
|
+
Array.from(row.children).forEach(cell => {
|
51
|
+
if (cell.tagName === 'TD' || cell.tagName == 'TH') {
|
52
|
+
cell.classList.remove('ignored');
|
53
|
+
}
|
54
|
+
});
|
55
|
+
});
|
56
|
+
|
57
|
+
selects.forEach(selectElement => {
|
58
|
+
if (selectElement.value === "") {
|
59
|
+
const headerCell = selectElement.closest('th');
|
60
|
+
if (headerCell && headerCell.parentElement) {
|
61
|
+
const columnIndex = Array.from(headerCell.parentElement.children).indexOf(headerCell);
|
62
|
+
|
63
|
+
if (columnIndex !== -1) {
|
64
|
+
dataRows.forEach(row => {
|
65
|
+
const cell = row.children[columnIndex];
|
66
|
+
if (cell && (cell.tagName === 'TD' || cell.tagName === 'TH')) {
|
67
|
+
cell.classList.add('ignored');
|
68
|
+
}
|
69
|
+
});
|
70
|
+
}
|
71
|
+
}
|
72
|
+
}
|
73
|
+
});
|
74
|
+
};
|
75
|
+
|
76
|
+
updateIgnoredColumns();
|
77
|
+
|
78
|
+
selects.forEach(selectElement => {
|
79
|
+
selectElement.addEventListener('change', updateIgnoredColumns);
|
80
|
+
});
|
81
|
+
}
|
82
|
+
</script>
|
data/config/routes.rb
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Importance
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
6
|
+
desc "Creates an Importance initializer for your application"
|
7
|
+
|
8
|
+
def copy_initializer
|
9
|
+
template "importance.rb", "config/initializers/importance.rb"
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_route
|
13
|
+
route 'mount Importance::Engine, at: "/"'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# Importance gem configuration
|
2
|
+
# Generated on <%= Date.today.strftime("%Y-%m-%d") %>
|
3
|
+
|
4
|
+
Importance.configure do |config|
|
5
|
+
# Set the layout to be used for the form. Can be :default or :bootstrap
|
6
|
+
config.set_layout :bootstrap
|
7
|
+
|
8
|
+
# Example importer configuration:
|
9
|
+
config.register_importer :students do |importer|
|
10
|
+
# Define required attributes with possible column names
|
11
|
+
importer.attribute :first_name, [ "First Name", "FirstName", "fname" ]
|
12
|
+
importer.attribute :last_name, [ "Last Name", "LastName", "lname" ]
|
13
|
+
importer.attribute :email, [ "Email", "E-Mail", "email", "mail" ]
|
14
|
+
|
15
|
+
# Process records in batches of this size
|
16
|
+
importer.batch_size 500
|
17
|
+
|
18
|
+
# Setup runs before the import begins
|
19
|
+
importer.setup do
|
20
|
+
@total_count = 0
|
21
|
+
@errors = []
|
22
|
+
|
23
|
+
# Access controller context and request info
|
24
|
+
@current_user_id = current_user.id
|
25
|
+
@import_source = request.remote_ip
|
26
|
+
|
27
|
+
# Initialize any resources needed for the import
|
28
|
+
@logger = Logger.new(Rails.root.join("log/imports.log"))
|
29
|
+
@logger.info("Starting import by #{current_user.email}")
|
30
|
+
end
|
31
|
+
|
32
|
+
# Main import logic
|
33
|
+
importer.on_complete do |records|
|
34
|
+
@total_count += records.size
|
35
|
+
|
36
|
+
records.each do |record|
|
37
|
+
begin
|
38
|
+
Student.create!(
|
39
|
+
first_name: record[:first_name],
|
40
|
+
last_name: record[:last_name],
|
41
|
+
email: record[:email],
|
42
|
+
created_by: @current_user_id
|
43
|
+
)
|
44
|
+
rescue => e
|
45
|
+
@errors << { record: record, message: e.message }
|
46
|
+
@logger.error("Error importing #{record}: #{e.message}")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Teardown runs after the import finishes
|
52
|
+
importer.teardown do
|
53
|
+
# Log import results
|
54
|
+
@logger.info("Import completed: #{@total_count} records processed with #{@errors.size} errors")
|
55
|
+
|
56
|
+
# Create an audit record
|
57
|
+
ImportAudit.create!(
|
58
|
+
user_id: @current_user_id,
|
59
|
+
records_count: @total_count,
|
60
|
+
errors_count: @errors.size,
|
61
|
+
source: @import_source
|
62
|
+
)
|
63
|
+
|
64
|
+
# Clean up resources
|
65
|
+
@logger.close
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
|
3
|
+
module Importance
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :importers, :layout
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@importers = {}
|
9
|
+
@layout = :blank
|
10
|
+
end
|
11
|
+
|
12
|
+
def register_importer(name, &block)
|
13
|
+
@importers[name] = Importer.new(name, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def set_layout(name)
|
17
|
+
@layout = name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Importer
|
22
|
+
attr_reader :name, :attributes, :batch, :setup_callback, :perform_callback, :teardown_callback, :error_callback
|
23
|
+
|
24
|
+
def initialize(name, &block)
|
25
|
+
@name = name
|
26
|
+
@attributes = []
|
27
|
+
@setup_callback = nil
|
28
|
+
@perform_callback = nil
|
29
|
+
@teardown_callback = nil
|
30
|
+
@error_callback = nil
|
31
|
+
@batch = false
|
32
|
+
instance_eval(&block) if block_given?
|
33
|
+
end
|
34
|
+
|
35
|
+
def attribute(key, labels)
|
36
|
+
@attributes << OpenStruct.new(key: key, labels: labels)
|
37
|
+
end
|
38
|
+
|
39
|
+
def batch_size(size)
|
40
|
+
@batch = size
|
41
|
+
end
|
42
|
+
|
43
|
+
def setup(&block)
|
44
|
+
@setup_callback = block
|
45
|
+
end
|
46
|
+
|
47
|
+
def perform(&block)
|
48
|
+
@perform_callback = block
|
49
|
+
end
|
50
|
+
|
51
|
+
def teardown(&block)
|
52
|
+
@teardown_callback = block
|
53
|
+
end
|
54
|
+
|
55
|
+
def error(&block)
|
56
|
+
@error_callback = block
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.configure
|
61
|
+
yield(configuration)
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.configuration
|
65
|
+
@configuration ||= Configuration.new
|
66
|
+
end
|
67
|
+
end
|
data/lib/importance/version.rb
CHANGED
data/lib/importance.rb
CHANGED
@@ -1,6 +1,25 @@
|
|
1
1
|
require "importance/version"
|
2
2
|
require "importance/engine"
|
3
|
+
require "importance/configuration"
|
4
|
+
require "generators/importance/install/install_generator" if defined?(Rails::Generators)
|
3
5
|
|
4
6
|
module Importance
|
5
|
-
|
7
|
+
class << self
|
8
|
+
attr_writer :configuration
|
9
|
+
|
10
|
+
def configuration
|
11
|
+
@configuration ||= Configuration.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# Alias for YourImporterGemName.configuration
|
15
|
+
def config
|
16
|
+
configuration
|
17
|
+
end
|
18
|
+
|
19
|
+
# Yields the singleton configuration object to a block.
|
20
|
+
# Used in the Rails initializer.
|
21
|
+
def configure
|
22
|
+
yield(configuration)
|
23
|
+
end
|
24
|
+
end
|
6
25
|
end
|
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.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lukas_Skywalker
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-06-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -16,18 +16,63 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 7.
|
19
|
+
version: 7.0.2
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 7.
|
27
|
-
|
28
|
-
|
26
|
+
version: 7.0.2
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: xsv
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: ostruct
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.6'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.6'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: debug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.10'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.10'
|
69
|
+
description: Importance is a Rails engine that allows users to upload Excel and CSV
|
70
|
+
files and interactively map columns to model attributes. It handles files with arbitrary
|
71
|
+
headers by letting users select which columns to import and which to ignore, with
|
72
|
+
support for flexible attribute mapping, batch processing, error handling, and customizable
|
73
|
+
import workflows.
|
29
74
|
email:
|
30
|
-
-
|
75
|
+
- git@lukasdiener.ch
|
31
76
|
executables: []
|
32
77
|
extensions: []
|
33
78
|
extra_rdoc_files: []
|
@@ -35,16 +80,27 @@ files:
|
|
35
80
|
- MIT-LICENSE
|
36
81
|
- README.md
|
37
82
|
- Rakefile
|
38
|
-
- app/assets/config/importance_manifest.js
|
39
83
|
- app/assets/stylesheets/importance/application.css
|
40
84
|
- app/controllers/importance/application_controller.rb
|
85
|
+
- app/controllers/importance/imports_controller.rb
|
41
86
|
- app/helpers/importance/application_helper.rb
|
42
87
|
- app/jobs/importance/application_job.rb
|
43
88
|
- app/mailers/importance/application_mailer.rb
|
44
89
|
- app/models/importance/application_record.rb
|
45
|
-
- app/
|
90
|
+
- app/models/importance/blank_layout.rb
|
91
|
+
- app/models/importance/bootstrap_layout.rb
|
92
|
+
- app/models/importance/header.rb
|
93
|
+
- app/views/importance/imports/map.html.erb
|
94
|
+
- config/locales/de.yml
|
95
|
+
- config/locales/en.yml
|
96
|
+
- config/locales/fr.yml
|
97
|
+
- config/locales/it.yml
|
46
98
|
- config/routes.rb
|
99
|
+
- lib/generators/importance/install/USAGE
|
100
|
+
- lib/generators/importance/install/install_generator.rb
|
101
|
+
- lib/generators/importance/install/templates/importance.rb
|
47
102
|
- lib/importance.rb
|
103
|
+
- lib/importance/configuration.rb
|
48
104
|
- lib/importance/engine.rb
|
49
105
|
- lib/importance/version.rb
|
50
106
|
- lib/tasks/importance_tasks.rake
|
@@ -71,8 +127,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
71
127
|
- !ruby/object:Gem::Version
|
72
128
|
version: '0'
|
73
129
|
requirements: []
|
74
|
-
rubygems_version: 3.
|
130
|
+
rubygems_version: 3.5.16
|
75
131
|
signing_key:
|
76
132
|
specification_version: 4
|
77
|
-
summary: Flexible
|
133
|
+
summary: Flexible Excel and CSV import engine with column mapping for Rails applications
|
78
134
|
test_files: []
|
@@ -1 +0,0 @@
|
|
1
|
-
//= link_directory ../stylesheets/importance .css
|