importance 0.1.0 → 0.2.1

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: a3ca5129019f5ae56f87a0f1fdecb67536010d39d93365f1ae46cb5bd41332ab
4
- data.tar.gz: eaa6be85de8f2d28a895d5a16a194b45e1bb46f01621a7b6b5a6ccae978ce9c3
3
+ metadata.gz: 8932ca3f775616358be95d4bb6164a807dfcb19b88e9edc850ce76e8591ea0c5
4
+ data.tar.gz: 995531acbf763a74ddcf7ca870f5889e489ca1812d3144dec83673b65e73f8f7
5
5
  SHA512:
6
- metadata.gz: f4e87f855969ab9da45721363e2b1d7bd17a9685381f10579ffa9de6df5b3b6267c78ac92ac6405d872926b50d87d09be3fa8b6a0419282255595b43ded63d63
7
- data.tar.gz: 67c9fe0b5f55256f657ac1e8a4c437a3a84e8412d712f28ee829fcc2b9ab1b937d9920a9817eb28b183e09aa5c61643138f12e10b3bf6560651a136ea5cc92e7
6
+ metadata.gz: 324d49edebde11322489529b0edc6fc492ae227ca6ded396e118bb6b4f0f3a04f6e907d1e8277c3c87d046555e1d5fa54e44ad3db9151db7ea99cabf9736d7fc
7
+ data.tar.gz: ffdda200cfb6d85b6dec0741a173ba57a794b75e5a24d642c3451526a9a96a33452b36df6ce12e0b47ca3f7771eac61bf7433dc9561d18411a09c0aabc0bcfa6
data/README.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # Importance
2
- Short description and motivation.
3
2
 
4
- ## Usage
5
- How to use my plugin.
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
- ## Releasing
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
- Run `rake build` to build the gem. Then run `rake release` to push it to rubygems.org.
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).
data/Rakefile CHANGED
@@ -6,3 +6,13 @@ load "rails/tasks/engine.rake"
6
6
  load "rails/tasks/statistics.rake"
7
7
 
8
8
  require "bundler/gem_tasks"
9
+
10
+ require "rake/testtask"
11
+
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << "test"
14
+ t.pattern = "test/**/*_test.rb"
15
+ t.verbose = false
16
+ end
17
+
18
+ task default: :test
@@ -1,4 +1,6 @@
1
1
  module Importance
2
- class ApplicationController < ActionController::Base
2
+ class ApplicationController < ::ApplicationController
3
+ include Rails.application.routes.url_helpers
4
+ helper Rails.application.routes.url_helpers
3
5
  end
4
6
  end
@@ -0,0 +1,129 @@
1
+ require "roo"
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
+ workbook = Roo::Spreadsheet.open(session[:path], { csv_options: { encoding: "bom|utf-8" } })
40
+ worksheet = workbook.sheet(0)
41
+ @file_headers = worksheet.row(1)
42
+ @samples = worksheet.parse[1..5]
43
+ @full_count = worksheet.count - 1
44
+
45
+ @importer_attributes = importer.attributes
46
+ @layout = "Importance::#{Importance.configuration.layout.to_s.camelize}Layout".constantize
47
+ end
48
+
49
+ # Import page. Load the file according to the mapping and import it.
50
+ # Mappings param is of the form mappings[excel_column_idx] = target_attribute
51
+ # mappings[0] = "first_name", mappings[1] = "", mappings[2] = "last_name" ...
52
+ def import
53
+ importer = Importance.configuration.importers[session[:importer].to_sym]
54
+ mappings = params[:mappings]
55
+
56
+ raise ArgumentError, "Mapping cannot be nil" if mappings.nil?
57
+
58
+ if importer.setup_callback
59
+ instance_exec(&importer.setup_callback)
60
+ end
61
+
62
+ begin
63
+ records_to_import = []
64
+
65
+ each_processed_row(mappings) do |record|
66
+ records_to_import << record
67
+
68
+ if importer.batch && records_to_import.size >= importer.batch
69
+ instance_exec(records_to_import, &importer.perform_callback)
70
+ records_to_import = []
71
+ end
72
+ end
73
+
74
+ if records_to_import.any?
75
+ instance_exec(records_to_import, &importer.perform_callback)
76
+ end
77
+
78
+ if importer.teardown_callback
79
+ instance_exec(&importer.teardown_callback)
80
+ else
81
+ redirect_to session[:redirect_url] || root_path, notice: "Import completed."
82
+ end
83
+
84
+ rescue => e
85
+ if importer.error_callback
86
+ instance_exec(e, &importer.error_callback)
87
+ end
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def csv_file?
94
+ File.extname(session[:path]).downcase == ".csv"
95
+ end
96
+
97
+ # Yields each processed row (a hash of attribute => value) to the given block.
98
+ # Skips empty rows (all values nil or empty).
99
+ def each_processed_row(mappings)
100
+ workbook = Roo::Spreadsheet.open(session[:path], { csv_options: { encoding: "bom|utf-8" } })
101
+ worksheet = workbook.sheet(0)
102
+ worksheet.each_with_index do |row, idx|
103
+ next if idx == 0 # Skip header row
104
+ record = process_row(row, mappings)
105
+ next if record.empty? || record.values.all? { |v| v.nil? || v.to_s.strip.empty? }
106
+ yield record
107
+ end
108
+ end
109
+
110
+ # Turn a row of the form ["Hans", "Robert", 1970, "male", "Apple Inc.", "hr@apple.com"]
111
+ # and a mapping of the form {"0"=>"first_name", "1"=>"last_name", "2"=>"", "3"=>"", "4"=>"", "5"=>"email"}
112
+ # into a record of the form { first_name: "Hans", last_name: "Robert", email: "hr@apple.com" }
113
+ def process_row(row, mappings)
114
+ record = {}
115
+
116
+ mappings.each do |column_index, attribute_name|
117
+ next if attribute_name.nil? || attribute_name == ""
118
+ value = row[column_index.to_i]
119
+ record[attribute_name.to_sym] = value
120
+ end
121
+
122
+ record
123
+ end
124
+
125
+ def rails_routes
126
+ ::Rails.application.routes.url_helpers
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,19 @@
1
+ module Importance
2
+ class BlankLayout
3
+ def self.select_class
4
+ ""
5
+ end
6
+
7
+ def self.submit_class
8
+ ""
9
+ end
10
+
11
+ def self.table_class
12
+ ""
13
+ end
14
+
15
+ def self.wrapper_class
16
+ ""
17
+ end
18
+ end
19
+ 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,85 @@
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_with_index do |file_header, file_header_idx| %>
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_idx}]",
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_with_index do |file_header, file_header_idx| %>
33
+ <td><%= sample[file_header_idx] %></td>
34
+ <% end %>
35
+ </tr>
36
+ <% end %>
37
+ </tbody>
38
+ </table>
39
+ <p>
40
+ <%= t('importance.import_description', count: @samples.count, full_count: @full_count) %>
41
+ </p>
42
+ </div>
43
+ <% end %>
44
+
45
+ <script>
46
+ const table = document.querySelector('.importance-table');
47
+ if (table) {
48
+ const selects = table.querySelectorAll('thead tr:first-child th select');
49
+ const dataRows = table.querySelectorAll('tbody tr, thead tr');
50
+
51
+ const updateIgnoredColumns = () => {
52
+ dataRows.forEach(row => {
53
+ Array.from(row.children).forEach(cell => {
54
+ if (cell.tagName === 'TD' || cell.tagName == 'TH') {
55
+ cell.classList.remove('ignored');
56
+ }
57
+ });
58
+ });
59
+
60
+ selects.forEach(selectElement => {
61
+ if (selectElement.value === "") {
62
+ const headerCell = selectElement.closest('th');
63
+ if (headerCell && headerCell.parentElement) {
64
+ const columnIndex = Array.from(headerCell.parentElement.children).indexOf(headerCell);
65
+
66
+ if (columnIndex !== -1) {
67
+ dataRows.forEach(row => {
68
+ const cell = row.children[columnIndex];
69
+ if (cell && (cell.tagName === 'TD' || cell.tagName === 'TH')) {
70
+ cell.classList.add('ignored');
71
+ }
72
+ });
73
+ }
74
+ }
75
+ }
76
+ });
77
+ };
78
+
79
+ updateIgnoredColumns();
80
+
81
+ selects.forEach(selectElement => {
82
+ selectElement.addEventListener('change', updateIgnoredColumns);
83
+ });
84
+ }
85
+ </script>
@@ -0,0 +1,6 @@
1
+ de:
2
+ importance:
3
+ use_column_as: Spalte verwenden als
4
+ ignore: Ignorieren
5
+ import: Importieren
6
+ import_description: "Es werden %{count} von %{full_count} Beispieldatensätzen angezeigt."
@@ -0,0 +1,6 @@
1
+ en:
2
+ importance:
3
+ use_column_as: Use column as
4
+ ignore: Ignore
5
+ import: Import
6
+ import_description: "Showing %{count} of %{full_count} sample records."
@@ -0,0 +1,6 @@
1
+ fr:
2
+ importance:
3
+ use_column_as: Utiliser la colonne comme
4
+ ignore: Ignorer
5
+ import: Importer
6
+ import_description: "Affichage de %{count} sur %{full_count} exemples d'enregistrements."
@@ -0,0 +1,6 @@
1
+ it:
2
+ importance:
3
+ use_column_as: Utilizzare come
4
+ ignore: Ignora
5
+ import: Importare
6
+ import_description: "Mostrati %{count} di %{full_count} record di esempio."
data/config/routes.rb CHANGED
@@ -1,2 +1,5 @@
1
1
  Importance::Engine.routes.draw do
2
+ post "/importance/submit", to: "imports#submit", as: :submit
3
+ get "/importance/map", to: "imports#map", as: :map
4
+ post "/importance/import", to: "imports#import", as: :import
2
5
  end
@@ -0,0 +1,9 @@
1
+ Description:
2
+ The importance:install generator creates an initializer file at
3
+ config/initializers/importance.rb with a basic configuration.
4
+
5
+ Examples:
6
+ rails generate importance:install
7
+
8
+ This will create:
9
+ config/initializers/importance.rb
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Importance
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.1"
3
3
  end
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
- # Your code goes here...
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.1.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lukas_Skywalker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-23 00:00:00.000000000 Z
11
+ date: 2025-10-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,18 +16,49 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 7.1.1
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.1.1
27
- description: Importance allows users to upload spreadsheets in any format and lets
28
- them select how the columns should be imported.
26
+ version: 7.0.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: roo
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
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: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.6'
55
+ description: Importance is a Rails engine that allows users to upload Excel and CSV
56
+ files and interactively map columns to model attributes. It handles files with arbitrary
57
+ headers by letting users select which columns to import and which to ignore, with
58
+ support for flexible attribute mapping, batch processing, error handling, and customizable
59
+ import workflows.
29
60
  email:
30
- - LukasSkywalker@users.noreply.github.com
61
+ - git@lukasdiener.ch
31
62
  executables: []
32
63
  extensions: []
33
64
  extra_rdoc_files: []
@@ -35,16 +66,27 @@ files:
35
66
  - MIT-LICENSE
36
67
  - README.md
37
68
  - Rakefile
38
- - app/assets/config/importance_manifest.js
39
69
  - app/assets/stylesheets/importance/application.css
40
70
  - app/controllers/importance/application_controller.rb
71
+ - app/controllers/importance/imports_controller.rb
41
72
  - app/helpers/importance/application_helper.rb
42
73
  - app/jobs/importance/application_job.rb
43
74
  - app/mailers/importance/application_mailer.rb
44
75
  - app/models/importance/application_record.rb
45
- - app/views/layouts/importance/application.html.erb
76
+ - app/models/importance/blank_layout.rb
77
+ - app/models/importance/bootstrap_layout.rb
78
+ - app/models/importance/header.rb
79
+ - app/views/importance/imports/map.html.erb
80
+ - config/locales/de.yml
81
+ - config/locales/en.yml
82
+ - config/locales/fr.yml
83
+ - config/locales/it.yml
46
84
  - config/routes.rb
85
+ - lib/generators/importance/install/USAGE
86
+ - lib/generators/importance/install/install_generator.rb
87
+ - lib/generators/importance/install/templates/importance.rb
47
88
  - lib/importance.rb
89
+ - lib/importance/configuration.rb
48
90
  - lib/importance/engine.rb
49
91
  - lib/importance/version.rb
50
92
  - lib/tasks/importance_tasks.rake
@@ -71,8 +113,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
71
113
  - !ruby/object:Gem::Version
72
114
  version: '0'
73
115
  requirements: []
74
- rubygems_version: 3.4.10
116
+ rubygems_version: 3.5.22
75
117
  signing_key:
76
118
  specification_version: 4
77
- summary: Flexible importer for Excel and CSV files.
119
+ summary: Flexible Excel and CSV import engine with column mapping for Rails applications
78
120
  test_files: []
@@ -1 +0,0 @@
1
- //= link_directory ../stylesheets/importance .css
@@ -1,15 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Importance</title>
5
- <%= csrf_meta_tags %>
6
- <%= csp_meta_tag %>
7
-
8
- <%= stylesheet_link_tag "importance/application", media: "all" %>
9
- </head>
10
- <body>
11
-
12
- <%= yield %>
13
-
14
- </body>
15
- </html>