ixport 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 +85 -4
- data/app/controllers/ixport/application_controller.rb +5 -1
- data/app/controllers/ixport/export_templates_controller.rb +52 -0
- data/app/controllers/ixport/exports_controller.rb +18 -0
- data/app/controllers/ixport/imports_controller.rb +4 -3
- data/app/controllers/ixport/previews_controller.rb +2 -2
- data/app/controllers/ixport/runs_controller.rb +13 -1
- data/app/helpers/ixport/imports_helper.rb +13 -0
- data/app/models/ixport/csv_writer.rb +19 -0
- data/app/models/ixport/export_template.rb +19 -0
- data/app/models/ixport/import.rb +71 -19
- data/app/models/ixport/null_reporter.rb +11 -0
- data/app/models/ixport/row_walk.rb +1 -1
- data/app/models/ixport/wallflower_runner.rb +10 -0
- data/app/models/ixport/wallflower_task_runner.rb +14 -0
- data/app/views/ixport/export_templates/edit.html.erb +14 -0
- data/app/views/ixport/export_templates/new.html.erb +11 -0
- data/app/views/ixport/exports/index.html.erb +23 -0
- data/app/views/ixport/imports/index.html.erb +17 -0
- data/app/views/ixport/imports/show.html.erb +30 -15
- data/config/routes.rb +4 -0
- data/lib/generators/ixport/install/install_generator.rb +3 -0
- data/lib/generators/ixport/install/templates/add_error_message_to_ixport_imports.rb.erb +5 -0
- data/lib/generators/ixport/install/templates/add_wallflower_task_to_ixport_imports.rb.erb +6 -0
- data/lib/generators/ixport/install/templates/create_ixport_export_templates.rb.erb +12 -0
- data/lib/generators/ixport/update/update_generator.rb +28 -0
- data/lib/ixport/background_runner.rb +23 -0
- data/lib/ixport/configuration.rb +2 -1
- data/lib/ixport/engine.rb +4 -1
- data/lib/ixport/exporter.rb +27 -0
- data/lib/ixport/registrations.rb +29 -3
- data/lib/ixport/version.rb +1 -1
- data/lib/ixport.rb +2 -0
- data/the_local/agents/ixport-develop.md +224 -29
- data/the_local/agents/ixport-info.md +55 -22
- data/the_local/agents/ixport-install.md +63 -38
- data/the_local/interface.yml +36 -1
- metadata +18 -1
data/config/routes.rb
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
Ixport::Engine.routes.draw do
|
|
2
2
|
root "imports#index"
|
|
3
|
+
resources :exports, only: %i[index show] do
|
|
4
|
+
resources :templates, only: %i[new create], controller: "export_templates"
|
|
5
|
+
end
|
|
6
|
+
resources :export_templates, only: %i[show edit update destroy]
|
|
3
7
|
resources :imports, only: %i[new create show] do
|
|
4
8
|
resource :mapping, only: %i[show update]
|
|
5
9
|
resource :preview, only: :show
|
|
@@ -15,6 +15,9 @@ module Ixport
|
|
|
15
15
|
def copy_migrations
|
|
16
16
|
migration_template "create_ixport_imports.rb.erb", "db/migrate/create_ixport_imports.rb"
|
|
17
17
|
migration_template "create_ixport_import_refusals.rb.erb", "db/migrate/create_ixport_import_refusals.rb"
|
|
18
|
+
migration_template "create_ixport_export_templates.rb.erb", "db/migrate/create_ixport_export_templates.rb"
|
|
19
|
+
migration_template "add_wallflower_task_to_ixport_imports.rb.erb", "db/migrate/add_wallflower_task_to_ixport_imports.rb"
|
|
20
|
+
migration_template "add_error_message_to_ixport_imports.rb.erb", "db/migrate/add_error_message_to_ixport_imports.rb"
|
|
18
21
|
end
|
|
19
22
|
end
|
|
20
23
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreateIxportExportTemplates < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :ixport_export_templates do |t|
|
|
4
|
+
t.string :key, null: false
|
|
5
|
+
t.string :name, null: false
|
|
6
|
+
t.json :columns, null: false
|
|
7
|
+
t.references :person, polymorphic: true, null: false
|
|
8
|
+
t.references :account, polymorphic: true, null: true
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module Ixport
|
|
7
|
+
module Generators
|
|
8
|
+
class UpdateGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("../install/templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Updates Ixport: copies the migrations added since it was installed."
|
|
14
|
+
|
|
15
|
+
def copy_export_templates_migration
|
|
16
|
+
migration_template "create_ixport_export_templates.rb.erb", "db/migrate/create_ixport_export_templates.rb"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def copy_wallflower_task_migration
|
|
20
|
+
migration_template "add_wallflower_task_to_ixport_imports.rb.erb", "db/migrate/add_wallflower_task_to_ixport_imports.rb"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def copy_error_message_migration
|
|
24
|
+
migration_template "add_error_message_to_ixport_imports.rb.erb", "db/migrate/add_error_message_to_ixport_imports.rb"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ixport
|
|
4
|
+
class ImportFailed < StandardError; end
|
|
5
|
+
|
|
6
|
+
def self.background_runner
|
|
7
|
+
case configuration.background_runner
|
|
8
|
+
when nil then nil
|
|
9
|
+
when :wallflower then WallflowerRunner
|
|
10
|
+
else configuration.background_runner.to_s.constantize
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.run_import(import, reporter: NullReporter.new)
|
|
15
|
+
import.run!(reporter: reporter)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.install_background_runner!
|
|
19
|
+
return unless configuration.background_runner == :wallflower
|
|
20
|
+
|
|
21
|
+
Wallflower.register_kind(:ixport_import, title: "Import", runner: "Ixport::WallflowerTaskRunner")
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/ixport/configuration.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Ixport
|
|
4
4
|
class Configuration
|
|
5
|
-
attr_accessor :authentication_method, :current_person_method, :current_account_method, :layout, :inline_row_limit
|
|
5
|
+
attr_accessor :authentication_method, :current_person_method, :current_account_method, :layout, :inline_row_limit, :background_runner
|
|
6
6
|
|
|
7
7
|
def initialize
|
|
8
8
|
@authentication_method = :authenticate_user!
|
|
@@ -10,6 +10,7 @@ module Ixport
|
|
|
10
10
|
@current_account_method = :current_account
|
|
11
11
|
@layout = "application"
|
|
12
12
|
@inline_row_limit = 1_000
|
|
13
|
+
@background_runner = nil
|
|
13
14
|
end
|
|
14
15
|
end
|
|
15
16
|
|
data/lib/ixport/engine.rb
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ixport
|
|
4
|
+
class Exporter
|
|
5
|
+
Column = Struct.new(:key, :label, :through, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
attr_reader :person, :account
|
|
8
|
+
|
|
9
|
+
def initialize(person:, account:)
|
|
10
|
+
@person = person
|
|
11
|
+
@account = account
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.column(key, label:, through: nil)
|
|
15
|
+
columns << Column.new(key: key.to_sym, label: label, through: through)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.columns
|
|
19
|
+
@columns ||= []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def value(record, column)
|
|
23
|
+
source = column.through ? record.public_send(column.through) : record
|
|
24
|
+
source&.public_send(column.key)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/ixport/registrations.rb
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Ixport
|
|
4
|
-
|
|
4
|
+
module AccessRule
|
|
5
|
+
def allows?(person, account)
|
|
6
|
+
allow.nil? || allow.call(person, account)
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
Registration = Struct.new(:key, :title, :importer, :allow, keyword_init: true) { include AccessRule }
|
|
11
|
+
ExportRegistration = Struct.new(:key, :title, :exporter, :chosen, :allow, keyword_init: true) { include AccessRule }
|
|
5
12
|
|
|
6
13
|
class MissingImporter < StandardError; end
|
|
14
|
+
class MissingExporter < StandardError; end
|
|
7
15
|
|
|
8
|
-
def self.register_import(key, title:, importer:)
|
|
9
|
-
registrations[key.to_sym] = Registration.new(key: key.to_sym, title: title, importer: importer.to_s)
|
|
16
|
+
def self.register_import(key, title:, importer:, allow: nil)
|
|
17
|
+
registrations[key.to_sym] = Registration.new(key: key.to_sym, title: title, importer: importer.to_s, allow: allow)
|
|
10
18
|
end
|
|
11
19
|
|
|
12
20
|
def self.registration(key)
|
|
@@ -23,9 +31,27 @@ module Ixport
|
|
|
23
31
|
|
|
24
32
|
raise MissingImporter, "Ixport import #{registration.key} names the importer #{registration.importer}, which does not exist."
|
|
25
33
|
end
|
|
34
|
+
export_registrations.each_value do |registration|
|
|
35
|
+
next if registration.exporter.safe_constantize
|
|
36
|
+
|
|
37
|
+
raise MissingExporter, "Ixport export #{registration.key} names the exporter #{registration.exporter}, which does not exist."
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.register_export(key, title:, exporter:, chosen: false, allow: nil)
|
|
42
|
+
export_registrations[key.to_sym] = ExportRegistration.new(key: key.to_sym, title: title, exporter: exporter.to_s, chosen: chosen, allow: allow)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.export_registration(key)
|
|
46
|
+
export_registrations.fetch(key.to_sym)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.export_registrations
|
|
50
|
+
@export_registrations ||= {}
|
|
26
51
|
end
|
|
27
52
|
|
|
28
53
|
def self.reset_registrations!
|
|
29
54
|
@registrations = {}
|
|
55
|
+
@export_registrations = {}
|
|
30
56
|
end
|
|
31
57
|
end
|
data/lib/ixport/version.rb
CHANGED
data/lib/ixport.rb
CHANGED
|
@@ -1,38 +1,59 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ixport-develop
|
|
3
|
-
description: Use PROACTIVELY for setting up CSV imports in a Rails app that has ixport — registering a mapped import, writing its importer class, declaring the fields a person maps a CSV's columns to, choosing the field rows are matched on, scoping the records an import may add or update to the signed-in person or account, linking to an import's upload page, explaining the upload, column-matching, preview and result pages and their messages, explaining what running an import saves and refuses, and fixing a boot that fails with Ixport::MissingImporter — MUST BE USED instead of hand-writing a CSV upload controller, a column-mapping form, a row-matching loop, an import preview
|
|
3
|
+
description: Use PROACTIVELY for setting up CSV imports and exports in a Rails app that has ixport — registering a mapped import, writing its importer class, declaring the fields a person maps a CSV's columns to, choosing the field rows are matched on, scoping the records an import may add or update to the signed-in person or account, limiting which people may use an import or export with an access rule such as the app's own Pundit policy, linking to an import's upload page, explaining the upload, column-matching, preview and result pages and their messages, explaining the list of a person's past imports on the imports page, explaining what running an import saves and refuses, explaining what a person sees when an import fails partway and which rows it keeps, running imports in a background job through the app's own runner class, explaining what a person sees while an import runs in the background, registering a fixed export or a chosen export, writing its exporter class, declaring its columns including columns taken from a related record, computing a column's value, scoping the records an export may hold, letting an account save named templates of picked columns for an export, explaining the new, edit and delete template pages and what a template's download holds, linking to the exports page, one export's CSV download or one template's download, making the app's own import job fail with Ixport::ImportFailed, and fixing a boot that fails with Ixport::MissingImporter or Ixport::MissingExporter — MUST BE USED instead of hand-writing a CSV upload controller, a column-mapping form, a row-matching loop, an import preview, a loop that saves imported rows, a background job that saves imported rows, a rescue that records why an import stopped, a permission check in front of an import or export page, a CSV download action, a loop that writes records to a CSV, or a model and form for saved column picks.
|
|
4
4
|
tools: Bash, Read, Write, Edit, Grep
|
|
5
|
-
scope: CSV import and export — a mounted engine where a signed-in person sees the mapped imports the host app registers, each backed by an importer class the host writes
|
|
5
|
+
scope: CSV import and export — a mounted engine where a signed-in person sees the mapped imports and the exports the host app registers, each import backed by an importer class and each export by an exporter class the host writes, where an export is either fixed or chosen, and a chosen export lets the account save named templates of picked columns
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
This local adds mapped imports to a Rails app that already has ixport installed by following the steps below exactly. It writes one importer class and one registration line per import, and it invents no method, option, route or file that is not listed here.
|
|
8
|
+
This local adds mapped imports, fixed exports and chosen exports to a Rails app that already has ixport installed by following the steps below exactly. It writes one importer or exporter class and one registration line per import or export, and it invents no method, option, route or file that is not listed here.
|
|
9
9
|
|
|
10
10
|
## What ixport is
|
|
11
11
|
|
|
12
|
-
ixport is a Rails engine that lets a signed-in person import a host app's records from CSV files. The app registers each kind of CSV a person can import, called a mapped import, and writes an importer class that names the fields the CSV's columns map to, the one field rows are matched on, and the records a row may add to or update. A person picks an import, uploads a CSV file for it, matches each of the file's columns to one of the importer's fields, sees a preview of how many rows will add a record, update one or be refused, runs the import to save those rows, and then sees how many rows were added and updated and each refused row. Fire this local when a developer wants people to load one of the app's records from a spreadsheet, wants to link to an import's upload page, asks what the upload, column-matching, preview or
|
|
12
|
+
ixport is a Rails engine that lets a signed-in person import a host app's records from CSV files and download them as CSV files. The app registers each kind of CSV a person can import, called a mapped import, and writes an importer class that names the fields the CSV's columns map to, the one field rows are matched on, and the records a row may add to or update. A person picks an import, uploads a CSV file for it, matches each of the file's columns to one of the importer's fields, sees a preview of how many rows will add a record, update one or be refused, runs the import to save those rows, and then sees how many rows were added and updated and each refused row. When the run stops on an error, the import is marked failed, keeps the rows it saved before stopping, and its page shows the error. An import runs within the request by default, or, when the app is set up for it, in the background through wallflower or through a runner class the app writes. The imports page also lists the person's own past imports in the current account, each opening at the step it has reached. The app also registers each CSV a person can download, and writes an exporter class that names the columns the file can hold, in order, and the records it holds one row each for. An export is either fixed or chosen. A fixed export always holds every declared column, and a person downloads it from the exports page. A chosen export lets people in the current account save named templates, each a picked list of the exporter's columns in a picked order, and a person downloads a template from the exports page to get a file holding only those columns. Fire this local when a developer wants people to load one of the app's records from a spreadsheet or download them as one, wants people to pick which columns an export holds, wants to link to an import's upload page, the exports page, an export's download or a template's download, asks what the upload, column-matching, preview, result, exports or template pages do, asks what the list of past imports shows, asks what running an import saves, what happens when a run fails, or what an export's or template's file holds, wants imports run in a background job of the app's own, or when the app fails to boot with `Ixport::MissingImporter` or `Ixport::MissingExporter`.
|
|
13
13
|
|
|
14
14
|
## Interface
|
|
15
15
|
|
|
16
|
-
- `Ixport.register_import` — `Ixport.register_import(key, title:, importer:)` registers one mapped import under a key, with the title a person sees
|
|
16
|
+
- `Ixport.register_import` — `Ixport.register_import(key, title:, importer:, allow: nil)` registers one mapped import under a key, with the title a person sees, the name of its importer class, and optionally an access rule that decides which people may use it.
|
|
17
17
|
- `Ixport::Importer` — the base class every importer inherits from.
|
|
18
18
|
- `field` — `field key, label:` is called in an importer's class body and declares one field a CSV column can be mapped to, with the label a person sees.
|
|
19
19
|
- `match_on` — `match_on key` is called in an importer's class body and names the one declared field a row is matched to an existing record on.
|
|
20
|
-
- `records` — an instance method every importer defines
|
|
21
|
-
- `person` — returns the person
|
|
22
|
-
- `account` — returns the account
|
|
20
|
+
- `records` — an instance method every importer and every exporter defines: in an importer it returns the Active Record relation a row may update a record from and adds a new record to, and in an exporter it returns the records the CSV holds, one row each, in the order returned.
|
|
21
|
+
- `person` — returns the signed-in person, for use inside `records` and `value`: in an importer the person who uploaded the file, in an exporter the person downloading it.
|
|
22
|
+
- `account` — returns the current account, for use inside `records` and `value`: in an importer the account current when the file was uploaded, in an exporter the account current when the file is downloaded, and `nil` when the app has no current account.
|
|
23
23
|
- `Ixport::MissingImporter` — the error raised when the app boots if a registered import names an importer class that does not exist.
|
|
24
|
-
- `
|
|
24
|
+
- `Ixport::MissingExporter` — the error raised when the app boots if a registered export names an exporter class that does not exist.
|
|
25
|
+
- `Ixport::ImportFailed` — the error a background run is failed with when its import stopped on an error, carrying the import's error message: wallflower's task raises it, and the app's own job may raise it.
|
|
26
|
+
- `Ixport.run_import` — `Ixport.run_import(import)` or `Ixport.run_import(import, reporter: reporter)` runs one import that a runner was handed, as the signed-in person and in the account it was uploaded with, saving and refusing its rows the same way as a run within the request, marking the import failed rather than raising when the run stops on an error, and optionally reporting each row to `reporter`.
|
|
27
|
+
- `enqueue` — `enqueue(import)` is the class method the app's own runner class defines, which ixport calls with each import a person runs, once the import is marked running, so the runner can arrange for `Ixport.run_import` to run it later.
|
|
28
|
+
- `GET /` — the engine's root page, at the path the engine is mounted under, which lists every registered import by its title, each linking to that import's upload page, and then lists the signed-in person's past imports in the current account, newest first, each linking to the step it has reached.
|
|
25
29
|
- `GET /imports/new` — the upload page for one import, given the import's registered key as the `key` query parameter, which shows the import's title, a CSV file field and an Upload button.
|
|
26
30
|
- `POST /imports` — takes the `key` and `file` parameters, stores the uploaded file as an import belonging to the signed-in person and the current account, and redirects to that import's column-matching page.
|
|
27
31
|
- `GET /imports/:import_id/mapping` — the column-matching page, titled "Match columns", which lists each column header from the file's first row beside a select of the importer's field labels and a "Skip" choice.
|
|
28
32
|
- `PATCH /imports/:import_id/mapping` — saves which field each column maps to and redirects to the import's preview page.
|
|
29
|
-
- `GET /imports/:import_id/preview` — the preview page, titled "Preview", which shows how many rows will add a record, update one or be refused, lists each updating row and each refused row by its row number, saves nothing, and offers a "Run import" button, and which redirects to the import's result page once the import has
|
|
30
|
-
- `POST /imports/:import_id/run` — saves every row that adds or updates a record, records every refused row with its reason, marks the import finished, and redirects to the import's result page, and does nothing but redirect when the import has already
|
|
31
|
-
- `GET /imports/:id` — the result page, titled "Import finished", which shows how many rows the run added and updated and lists each refused row by its row number with its reason, and which redirects to the import's preview page when the import has
|
|
33
|
+
- `GET /imports/:import_id/preview` — the preview page, titled "Preview", which shows how many rows will add a record, update one or be refused, lists each updating row and each refused row by its row number, saves nothing, and offers a "Run import" button, and which redirects to the import's result page once the import has finished or failed, or while it is running in the background.
|
|
34
|
+
- `POST /imports/:import_id/run` — with no background runner, saves every row that adds or updates a record, records every refused row with its reason, marks the import finished, or failed when the run stops on an error, and redirects to the import's result page, and does nothing but redirect when the import has already finished or failed; with a background runner, marks a pending import running, hands it to the runner, and redirects to the wallflower task's page or, for the app's own runner, to the import's result page, and hands nothing to the runner and redirects to the import's result page when the import is already running, finished or failed.
|
|
35
|
+
- `GET /imports/:id` — the result page, titled "Import finished", which shows how many rows the run added and updated and lists each refused row by its row number with its reason, titled "Import running" with the message "This import is running." while a background run is under way, titled "Import failed" with the error the run stopped on and the counts of rows saved before it stopped when the run failed, and which redirects to the import's preview page when the import has neither run nor started running.
|
|
36
|
+
- `Ixport.register_export` — `Ixport.register_export(key, title:, exporter:, chosen: false, allow: nil)` registers one export under a key, with the title a person sees and the name of its exporter class, as a fixed export by default or as a chosen export when `chosen: true`, and optionally an access rule that decides which people may use it.
|
|
37
|
+
- `Ixport::Exporter` — the base class every exporter inherits from.
|
|
38
|
+
- `column` — `column key, label:, through: nil` is called in an exporter's class body and declares one column of the CSV, with the header a person sees, read from the record's `key` attribute, or from the `key` attribute of the related record named by `through:`.
|
|
39
|
+
- `value` — `value(record, column)` is the instance method that returns one cell of the CSV for one record and one declared column, and an exporter may override it to compute a cell.
|
|
40
|
+
- `GET /exports` — the exports page, titled "Exports", which lists every registered export by its title: a fixed export links to its download, and a chosen export lists the current account's templates for it with a "New template" button.
|
|
41
|
+
- `GET /exports/:id` — downloads the CSV of the export registered under the key given as `:id`, holding every declared column, as a file named after that key.
|
|
42
|
+
- `GET /exports/:export_id/templates/new` — the new template page for the chosen export registered under the key given as `:export_id`, which shows a "Template name" field and one column select per declared column.
|
|
43
|
+
- `POST /exports/:export_id/templates` — takes the `name` and `columns[]` parameters, saves a template for that chosen export belonging to the signed-in person and the current account, and redirects to the exports page.
|
|
44
|
+
- `GET /export_templates/:id` — downloads the CSV of one template, holding only its picked columns in its picked order, as a file named after the template's name.
|
|
45
|
+
- `GET /export_templates/:id/edit` — the edit template page, which shows the template's name and picked columns in the same fields as the new template page, and a Delete action.
|
|
46
|
+
- `PATCH /export_templates/:id` — takes the `name` and `columns[]` parameters, replaces the template's name and picked columns, and redirects to the exports page.
|
|
47
|
+
- `DELETE /export_templates/:id` — deletes the template and redirects to the exports page.
|
|
32
48
|
|
|
33
49
|
## How to use it
|
|
34
50
|
|
|
35
|
-
1
|
|
51
|
+
Steps 1 to 24 add a mapped import, and steps 19 to 23 of those cover running imports in the background. Steps 25 to 37 add an export, fixed or chosen. Steps 38 to 44 cover the templates of a chosen export. Do step 1 first in every case.
|
|
52
|
+
|
|
53
|
+
1. Check that ixport is installed: `bin/rails routes` lists a path served by ixport. If it does not, stop and hand the setup to the `ixport-install` local before going on. For any import, also check that `db/migrate/` has an `add_error_message_to_ixport_imports` migration and that it has run, because a run that stops on an error writes the column it adds, and without it the run raises an error naming the missing column. If it does not, hand the update to the `ixport-install` local before going on. To run imports in the background, also check that `db/migrate/` has an `add_wallflower_task_to_ixport_imports` migration and that it has run, because pressing "Run import" with any background runner reads the column it adds, including with the app's own runner. If it does not, hand the update to the `ixport-install` local before going on. For a chosen export, also check that `db/migrate/` has a `create_ixport_export_templates` migration and that it has run. If it does not, hand the update to the `ixport-install` local before going on.
|
|
54
|
+
|
|
55
|
+
### A mapped import
|
|
56
|
+
|
|
36
57
|
2. Ask the developer which of the app's records the import loads, such as products or customers. Do not pick one.
|
|
37
58
|
3. Ask the developer which attributes of that record a person may import, and the label a person should see for each, such as "SKU" for `sku` or "Price" for `price_cents`. Do not decide which attributes are importable.
|
|
38
59
|
4. Ask the developer which of those fields identifies an existing record, such as the SKU or the email. Do not pick the match field.
|
|
@@ -62,16 +83,34 @@ ixport is a Rails engine that lets a signed-in person import a host app's record
|
|
|
62
83
|
Ixport.register_import :products, title: "Products", importer: "ProductImporter"
|
|
63
84
|
```
|
|
64
85
|
Pass `importer:` the class name as a string, never the class itself, so the initializer does not load the class before the app's code is ready.
|
|
86
|
+
|
|
87
|
+
Ask the developer whether every signed-in person may use this import, or only some. Do not decide who may. When only some may, pass `allow:` a rule that takes the signed-in person and the current account and returns true for a person who may use it, such as a call to the app's own Pundit policy:
|
|
88
|
+
```ruby
|
|
89
|
+
Ixport.register_import :products, title: "Products", importer: "ProductImporter",
|
|
90
|
+
allow: ->(person, account) { ProductImportPolicy.new(person, account).create? }
|
|
91
|
+
```
|
|
92
|
+
- A registration with no `allow:` is open to every signed-in person.
|
|
93
|
+
- The rule is called on every request to the import's pages and on every visit to the engine's root page, so it must not change anything.
|
|
94
|
+
- `account` is `nil` when the app has no current account, so the rule must handle that when it can happen.
|
|
95
|
+
- A person the rule refuses does not see the import on the engine's root page, does not see their own past imports of it under "Your imports", and gets a not found response from its upload, column-matching, preview, run and result pages, including for imports they uploaded before the rule refused them.
|
|
96
|
+
- The rule is checked within the person's request only. A background run already handed to a runner still runs to the end.
|
|
65
97
|
9. Start the app. If it fails to boot with `Ixport::MissingImporter`, the message names the import key and the class name that does not exist. Fix the class name in the registration, or the class name or file name of the importer, so the two match.
|
|
66
|
-
10. Visit the path the engine is mounted under while signed in. The page shows the heading "Imports" and lists each registered import by its title, in the order the imports were registered. With no import registered it shows "No imports are set up yet."
|
|
98
|
+
10. Visit the path the engine is mounted under while signed in. The page shows the heading "Imports" and lists each registered import the signed-in person is allowed to use by its title, in the order the imports were registered. With no import registered, or none the person is allowed to use, it shows "No imports are set up yet." Below that, once the signed-in person has uploaded any file, a "Your imports" section lists their past imports:
|
|
99
|
+
- It lists only imports uploaded by the signed-in person in the current account, newest first, of imports that are still registered and that the person is allowed to use. Imports by other people, or by the same person in another account, are not listed.
|
|
100
|
+
- Each shows the import's title, the uploaded file's name, its status, "Pending", "Running", "Finished" or "Failed", and the date it was uploaded, such as "Oct 10, 2026".
|
|
101
|
+
- A finished import also shows its "Added", "Updated" and "Refused" counts. A pending, running or failed import shows no counts.
|
|
102
|
+
- Each title links to the step the import has reached: the "Import finished" page once it has run, the "Preview" page once its mapping is saved with at least one column mapped, and otherwise the "Match columns" page. A running import's link leads to its "Import running" page, and a failed import's link leads to its "Import failed" page.
|
|
103
|
+
- When the person has no past import, the section is not shown.
|
|
67
104
|
11. When the developer wants a link to one import's upload page from the app's own pages, ask where the link goes. Build its URL with the engine's route helper and the import's registered key:
|
|
68
105
|
```erb
|
|
69
106
|
<%= link_to "Import products", ixport.new_import_path(key: :products) %>
|
|
70
107
|
```
|
|
71
|
-
The key must be one that is registered. An unregistered key gets a not found response.
|
|
108
|
+
The key must be one that is registered. An unregistered key, or one whose rule refuses the person, gets a not found response. When the import has an `allow:` rule, show the link only to people the same rule allows.
|
|
72
109
|
12. On the upload page a person chooses a CSV file and presses Upload. The upload is refused, and the upload page is shown again with status 422 and one of these messages, when:
|
|
110
|
+
- the file is not UTF-8 text: "This file is not UTF-8 text. Save it as UTF-8 and upload it again."
|
|
111
|
+
- the file is not valid CSV, such as a quote left open: "This file could not be read past line N. Check the quotes on that line."
|
|
73
112
|
- the file has no header row: "This file has no header row."
|
|
74
|
-
- the file has more data rows than the inline row limit, not counting the header row: "This file has N rows, and the most an import can have is L." The limit is set by `ixport-install
|
|
113
|
+
- the file has more data rows than the inline row limit, not counting the header row: "This file has N rows, and the most an import can have is L." The limit is set by `ixport-install`, and it applies only when imports run within the request. With a background runner, a file of any length is accepted.
|
|
75
114
|
13. After an upload is accepted, the person is on the "Match columns" page. Each column header from the file's first row is listed with a select of the importer's field labels. "Skip" leaves that column unmapped. Pressing "Save mapping" saves the choices and takes the person to the "Preview" page.
|
|
76
115
|
14. The preview decides each data row's outcome this way:
|
|
77
116
|
- The row's value for the match field is looked up in `records`, with one query for the whole file.
|
|
@@ -81,24 +120,180 @@ ixport is a Rails engine that lets a signed-in person import a host app's record
|
|
|
81
120
|
- A row whose added or updated record is not valid will be refused, with the record's own validation messages as the reason.
|
|
82
121
|
15. The preview page shows three counts, "Will add", "Will update" and "Will be refused". Under them it lists each row that will update a record under "Will update", and each refused row with its reason under "Will be refused", each by its row number in the file, where the header row is row 1. The refusal reasons are the record's validation messages, so tell the developer that the record's validations decide which rows are refused and what a person reads.
|
|
83
122
|
16. Viewing the preview adds and changes no record. Its back link returns the person to the "Match columns" page to change the mapping.
|
|
84
|
-
17.
|
|
123
|
+
17. When the app has no background runner, pressing "Run import" on the preview page runs the import within the same request, with no background job. Steps 19 to 23 cover an app with a background runner, where rows are saved and refused by the same rules. The run:
|
|
85
124
|
- Every row's outcome is decided again by the rules in step 14, against the records as they are at that moment, so the result can differ from the preview when records changed in between.
|
|
86
125
|
- Every row that adds or updates a record is saved with `save!`, so the record's callbacks run as for any other save.
|
|
87
126
|
- Every refused row is recorded with its row number and reason, and no record is saved for it.
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
|
|
91
|
-
|
|
127
|
+
- Rows are saved in file order, and each row's save is undone on its own when it raises.
|
|
128
|
+
- When anything raises during the run, such as a save hitting a database constraint the record's validations do not cover, the run stops at that row. The rows saved and refused before it stay saved, the rows after it are neither saved nor refused, and the import is marked failed with the error's message and the counts of rows added and updated before it stopped.
|
|
129
|
+
- The error is reported through Rails' error reporter, so it reaches whatever error service the app has subscribed there. It is not raised to the person's request.
|
|
130
|
+
- Tell the developer to cover every database constraint on the record with a validation, so a bad row is refused rather than stopping the run.
|
|
131
|
+
- An import runs once. A second run request for the same import, including one sent at the same time as the first, adds and changes nothing and redirects to the result page. A failed import is not run again, so the rows after the one it stopped on can only be loaded by uploading them as a new import.
|
|
132
|
+
18. After the run, the person is on the result page:
|
|
133
|
+
- When the run finished, the page is titled "Import finished". It shows two counts, "Added" and "Updated", and, when any row was refused, lists each refused row under "Refused" by its row number with its reason.
|
|
134
|
+
- When the run failed, the page is titled "Import failed". It shows the error's message, the line "Rows saved before it stopped stay saved.", the "Added" and "Updated" counts of rows saved before it stopped, and each row refused before it stopped.
|
|
135
|
+
- The error's message is the exception's own message, such as a database's constraint error, so tell the developer that a person reads it as written.
|
|
136
|
+
- Returning to this page later shows the same page. Visiting it for an import that has neither run nor started running redirects to its preview page. Once an import has finished or failed, its preview page redirects to this page, so it offers no "Run import" button.
|
|
137
|
+
|
|
138
|
+
### Running imports in the background
|
|
139
|
+
|
|
140
|
+
Whether imports run within the request, through wallflower, or through the app's own runner class is set by `ixport-install`. Steps 19 to 23 apply only when one of the two background runners is set.
|
|
141
|
+
|
|
142
|
+
19. With a background runner, pressing "Run import" marks the import running and hands it to the runner, and saves no row within the request:
|
|
143
|
+
- With wallflower, the person is taken to the wallflower task's page, which shows the run's progress, the rows it changed and refused, and a link to the import's result page once the run ends. When the run failed, the task fails with `Ixport::ImportFailed` carrying the import's error message, and still links to the import's "Import failed" page.
|
|
144
|
+
- With the app's own runner, the person is taken to the import's result page, titled "Import running", with the message "This import is running." and no counts. Once the run ends, the same page is titled "Import finished" or "Import failed" and shows what step 18 describes.
|
|
145
|
+
- While the import runs, its preview page redirects to its "Import running" page, so it offers no "Run import" button, and the imports page lists it as "Running" with a link that leads to that page.
|
|
146
|
+
- An import is handed to the runner once. A second run request for an import that is already running, finished or failed, such as one sent from a preview page left open, hands nothing to the runner and redirects to the import's result page.
|
|
147
|
+
20. For the app's own runner, ask the developer which background job system the app uses and which queue an import's job goes on. Do not pick either.
|
|
148
|
+
21. Write the runner class under the class name `ixport-install` set, with a class method `enqueue` that takes the import and starts a job, and write the job so it calls `Ixport.run_import` with the import:
|
|
149
|
+
```ruby
|
|
150
|
+
class ImportRunner
|
|
151
|
+
def self.enqueue(import)
|
|
152
|
+
RunImportJob.perform_later(import)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
```
|
|
156
|
+
```ruby
|
|
157
|
+
class RunImportJob < ApplicationJob
|
|
158
|
+
def perform(import)
|
|
159
|
+
Ixport.run_import(import)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
- Pass the import itself to the job, as above, never its id, because the job gets the import back only from what it was passed.
|
|
164
|
+
- `enqueue` is called within the person's request, so it must start the job and return, never run the import itself.
|
|
165
|
+
- The value `enqueue` returns is not used.
|
|
166
|
+
22. `Ixport.run_import` runs the import by the rules in steps 14 and 17:
|
|
167
|
+
- It builds the importer with the person who uploaded the file and the account current at that upload, so the job needs no signed-in person and `records` is scoped as it was for the preview.
|
|
168
|
+
- It adds and changes nothing when the import has already finished or failed.
|
|
169
|
+
- When the run stops on an error, it keeps the rows saved before it, marks the import failed, reports the error through Rails' error reporter, and returns without raising, as in step 17. The job therefore succeeds, and a retry of it would change nothing.
|
|
170
|
+
- Ask the developer whether a failed import should also fail the job, such as to show it in the job system's failed list. If it should, raise `Ixport::ImportFailed` with the import's error message after the run, and turn off retries for it, since a retry changes nothing:
|
|
171
|
+
```ruby
|
|
172
|
+
def perform(import)
|
|
173
|
+
Ixport.run_import(import)
|
|
174
|
+
raise Ixport::ImportFailed, import.error_message if import.failed?
|
|
175
|
+
end
|
|
176
|
+
```
|
|
177
|
+
- When `enqueue` raises, or the job never runs, the import stays running and its page keeps showing "This import is running.". Tell the developer this, and ask how they want such imports noticed.
|
|
178
|
+
23. When the developer wants to show a run's progress somewhere of their own, pass `reporter:` an object that answers all three of these, and ask the developer where the progress is shown:
|
|
179
|
+
- `set_total(total)` is called once, before the first row, with the number of data rows.
|
|
180
|
+
- `refuse(label:, reason:)` is called for each refused row, with a label such as "Row 4" and the refusal reason.
|
|
181
|
+
- `advance` is called once after each row, saved or refused.
|
|
182
|
+
24. An import's column-matching, preview, run and result pages are visible only to the person who uploaded it, in the account it was uploaded in, while the import's `allow:` rule allows them. Anyone else, the same person in another account, and the same person once the rule refuses them, gets a not found response.
|
|
183
|
+
|
|
184
|
+
### An export
|
|
185
|
+
|
|
186
|
+
25. Ask the developer which of the app's records the export holds, such as products or customers. Do not pick one.
|
|
187
|
+
26. Ask the developer which columns the file can hold, in order, and the header a person should see for each, such as "SKU" for `sku`. For each column, ask whether it is an attribute of the record itself, an attribute of a related record such as the product's account name, or a value computed from the record. Do not decide which columns are exported.
|
|
188
|
+
27. Ask the developer which records the file holds, such as the current account's products, and in what order its rows go. Do not pick the scope or the order. An export that reaches every row of the table lets one account download another account's records.
|
|
189
|
+
28. Ask the developer whether the export is fixed or chosen. Do not pick one.
|
|
190
|
+
- Fixed: every download holds every column from step 26, in the declared order.
|
|
191
|
+
- Chosen: people in the current account save named templates that each pick some of those columns in an order of their own, and each template downloads only its picked columns.
|
|
192
|
+
29. Write the exporter class in `app/exporters/`, named after the record, inheriting from `Ixport::Exporter`. A fixed export and a chosen export use the same kind of class:
|
|
193
|
+
```ruby
|
|
194
|
+
class ProductExporter < Ixport::Exporter
|
|
195
|
+
column :sku, label: "SKU"
|
|
196
|
+
column :price_cents, label: "Price"
|
|
197
|
+
column :name, label: "Account", through: :account
|
|
198
|
+
|
|
199
|
+
def records
|
|
200
|
+
account.products.includes(:account).order(:sku)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
- Call `column` once per column from step 26, in the order the columns appear in the file. `label:` is required and is that column's header in the file's first row.
|
|
205
|
+
- For a chosen export, the labels are the choices in each column select on the template pages, in the declared order.
|
|
206
|
+
- Each column's key must be unique within the exporter, because a template records its picked columns by key.
|
|
207
|
+
- For an attribute of the record itself, the key is the attribute's name as a symbol.
|
|
208
|
+
- For an attribute of a related record, pass `through:` the name of the association on the record, and the key is the attribute's name on the related record. A record whose related record is `nil` gets an empty cell in that column.
|
|
209
|
+
- Define `records` to return the records from step 27, built from `person` or `account`, in the row order from step 27. The file has one row per record, in the order `records` returns them.
|
|
210
|
+
- When any column uses `through:`, add `includes` for each association it names to `records`, so the download does not run one query per row.
|
|
211
|
+
- `account` is `nil` when the app has no current account, so a `records` that calls a method on `account` fails in that case. Ask the developer whether that can happen in this app.
|
|
212
|
+
- Never define `initialize` in an exporter. ixport builds the exporter with the signed-in person and the current account.
|
|
213
|
+
30. For a computed column from step 26, declare it with `column` under a key of your choosing and override `value` to return it, calling `super` for every other column:
|
|
214
|
+
```ruby
|
|
215
|
+
def value(record, column)
|
|
216
|
+
return record.price_cents / 100.0 if column.key == :price
|
|
217
|
+
|
|
218
|
+
super
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
- `column.key` is the column's key as a symbol, and `column.label` is its header.
|
|
222
|
+
- Ask the developer how the computed value is written, such as how many decimal places, rather than picking a format.
|
|
223
|
+
- A template's download writes its cells through the same `value`, so a computed column is computed the same way in every template that picks it.
|
|
224
|
+
31. Every cell is written as the value's `to_s`, and a `nil` value is an empty cell. When the developer needs a date, time or amount written a particular way, compute that column in `value` as in step 30.
|
|
225
|
+
32. Ask the developer what key and title the export gets. The key is a short symbol that identifies the export, such as `:products`, and is also the fixed export's downloaded file name, such as `products.csv`. The title is what a person sees on the exports page, such as "Products".
|
|
226
|
+
33. Add the registration to the end of `config/initializers/ixport.rb`, and create the file if it does not exist. For a fixed export:
|
|
227
|
+
```ruby
|
|
228
|
+
Ixport.register_export :products, title: "Products", exporter: "ProductExporter"
|
|
229
|
+
```
|
|
230
|
+
For a chosen export, add `chosen: true`:
|
|
231
|
+
```ruby
|
|
232
|
+
Ixport.register_export :products, title: "Products", exporter: "ProductExporter", chosen: true
|
|
233
|
+
```
|
|
234
|
+
Pass `exporter:` the class name as a string, never the class itself, so the initializer does not load the class before the app's code is ready.
|
|
235
|
+
|
|
236
|
+
Ask the developer whether every signed-in person may use this export, or only some. Do not decide who may. When only some may, pass `allow:` a rule that takes the signed-in person and the current account and returns true for a person who may use it, the same way as an import's rule in step 8:
|
|
237
|
+
```ruby
|
|
238
|
+
Ixport.register_export :products, title: "Products", exporter: "ProductExporter", chosen: true,
|
|
239
|
+
allow: ->(person, account) { ProductExportPolicy.new(person, account).show? }
|
|
240
|
+
```
|
|
241
|
+
- A registration with no `allow:` is open to every signed-in person.
|
|
242
|
+
- A person the rule refuses does not see the export on the exports page, and gets a not found response from its download and, for a chosen export, from its new template page, its template creation, and every one of its templates' download, edit, update and delete actions.
|
|
243
|
+
- The rule covers the export as a whole. Every template of a chosen export is allowed or refused with it, and a template cannot carry a rule of its own.
|
|
244
|
+
34. Start the app. If it fails to boot with `Ixport::MissingExporter`, the message names the export key and the class name that does not exist. Fix the class name in the registration, or the class name or file name of the exporter, so the two match.
|
|
245
|
+
35. Visit `exports` under the path the engine is mounted under while signed in. The page shows the heading "Exports" and lists each registered export the signed-in person is allowed to use by its title, in the order the exports were registered. With no export registered, or none the person is allowed to use, it shows "No exports are set up yet." The engine's root page lists imports only and does not link to this page.
|
|
246
|
+
- A fixed export's title links to its download.
|
|
247
|
+
- A chosen export's title is not a link. Under it are the current account's templates for that export, ordered by name, each name linking to the template's download beside an "Edit" link, and then a "New template" button.
|
|
248
|
+
36. When the developer wants a link to the exports page or to one fixed export's download from the app's own pages, ask where the link goes. Build its URL with the engine's route helpers and the export's registered key:
|
|
249
|
+
```erb
|
|
250
|
+
<%= link_to "Exports", ixport.exports_path %>
|
|
251
|
+
<%= link_to "Download products", ixport.export_path(:products) %>
|
|
252
|
+
```
|
|
253
|
+
The key must be one that is registered. An unregistered key, or one whose rule refuses the person, gets a not found response. When the export has an `allow:` rule, show the link only to people the same rule allows. `export_path` also downloads a chosen export with every declared column, although the exports page does not link to it.
|
|
254
|
+
37. A download builds the whole file within the same request, with no background job and no row limit, from the records `records` returns for the person downloading it, in the account current at that moment. This holds for a template's download too. Tell the developer that an export over a very large table makes that request slow.
|
|
255
|
+
|
|
256
|
+
### The templates of a chosen export
|
|
257
|
+
|
|
258
|
+
38. Pressing "New template" on the exports page opens the page titled "New <export title> template", with a back link to the exports page. It shows a required "Template name" field and one select per declared column, labelled "Column 1", "Column 2" and so on. Each select offers every column's label and "None". Pressing "Save template" saves the template and returns the person to the exports page.
|
|
259
|
+
39. The picked columns are saved in this way:
|
|
260
|
+
- They are kept in the order of the selects, so "Column 1" is the file's first column.
|
|
261
|
+
- A select left at "None" adds nothing.
|
|
262
|
+
- A column picked in more than one select is kept once, at its first position.
|
|
263
|
+
- A template with no picked column produces a file with an empty header row and one empty row per record.
|
|
264
|
+
40. A template belongs to the current account. Every person in that account whom the export's `allow:` rule allows sees it on the exports page and can download, edit or delete it. A person in another account, or one the rule refuses, gets a not found response. When the app has no current account, every template saved with no account is shared by every signed-in person with no account. Tell the developer this, and ask whether it is what they want.
|
|
265
|
+
41. A template's name link downloads a CSV holding only its picked columns, in its picked order, with each column's label as the header and one row per record from `records`. The file is named after the template's name, lower-cased with spaces and punctuation replaced by hyphens, such as `price-list.csv` for "Price list".
|
|
266
|
+
42. Pressing "Edit" opens the page titled "Edit <template name>", with the export's title under it and a back link to the exports page. It shows the same fields as the new template page, filled with the template's name and picked columns. Pressing "Save template" replaces the name and picked columns by the rules in step 39 and returns the person to the exports page.
|
|
267
|
+
43. The edit page's action menu has a Delete item. It deletes the template and returns the person to the exports page.
|
|
268
|
+
44. When the developer wants a link to one template's download or edit page from the app's own pages, ask where the link goes, and build it with the engine's route helpers and the template:
|
|
269
|
+
```erb
|
|
270
|
+
<%= link_to template.name, ixport.export_template_path(template) %>
|
|
271
|
+
<%= link_to "Edit", ixport.edit_export_template_path(template) %>
|
|
272
|
+
<%= link_to "New template", ixport.new_export_template_path(:products) %>
|
|
273
|
+
```
|
|
274
|
+
`new_export_template_path` takes the chosen export's registered key. An unregistered key, the key of a fixed export, or the key of an export whose rule refuses the person, gets a not found response.
|
|
92
275
|
|
|
93
276
|
## Conventions
|
|
94
277
|
|
|
95
|
-
- Every registration names an importer class that exists. The app refuses to boot otherwise, so never register an import before its
|
|
96
|
-
- Each key is registered once. Registering the same key a second time replaces the first registration.
|
|
278
|
+
- Every registration names an importer or exporter class that exists. The app refuses to boot otherwise, so never register an import or export before its class is written.
|
|
279
|
+
- Each import key is registered once, and each export key is registered once. Registering the same key a second time replaces the first registration.
|
|
97
280
|
- Every importer declares `match_on` and defines `records`.
|
|
281
|
+
- Every exporter declares at least one `column` and defines `records`.
|
|
98
282
|
- `records` is always scoped to the `person` or `account` the developer chose, never left to the whole table without the developer saying so.
|
|
99
|
-
- An importer inherits from `Ixport::Importer` directly and declares all of its own fields and its own match field.
|
|
100
|
-
-
|
|
101
|
-
- When
|
|
102
|
-
- When an
|
|
103
|
-
-
|
|
104
|
-
-
|
|
283
|
+
- An importer inherits from `Ixport::Importer` directly and declares all of its own fields and its own match field. An exporter inherits from `Ixport::Exporter` directly and declares all of its own columns. None of these is inherited by a class that inherits from another importer or exporter.
|
|
284
|
+
- An exporter that overrides `value` calls `super` for every column it does not compute.
|
|
285
|
+
- When a record's attribute or association is renamed or removed, update the matching `field`, `match_on` and `column` lines in the same change.
|
|
286
|
+
- When an importer or exporter class is renamed, update its `importer:` or `exporter:` string in `config/initializers/ixport.rb` in the same change.
|
|
287
|
+
- When an import's or export's key is changed, update every link to its upload page or download in the same change.
|
|
288
|
+
- Past imports record their import's key. Before changing an import's key or removing its registration, tell the developer that past imports under the old key drop off "Your imports" on the engine's root page, and that opening one of their pages raises an error, and ask what should happen to those imports.
|
|
289
|
+
- Who may use an import or export is decided only by its `allow:` rule. Never write a before action, a policy check or a redirect in front of the engine's pages.
|
|
290
|
+
- When an import or export has an `allow:` rule, every link to it from the app's own pages is shown only to people the same rule allows.
|
|
291
|
+
- Saved templates record a chosen export's key and its picked column keys. Before changing a chosen export's key, removing its registration, or changing it to fixed, ask the developer what happens to the templates already saved for it:
|
|
292
|
+
- A template whose export key is no longer registered no longer shows on the exports page, and opening its download or edit page raises an error.
|
|
293
|
+
- A template whose export is changed to fixed no longer shows on the exports page, and its download and edit links still work.
|
|
294
|
+
- When a chosen export's column key is renamed or removed, saved templates that picked it drop that column from their downloads without saying so. Tell the developer before making that change.
|
|
295
|
+
- Never write an upload form, a column-matching form, a preview page, a run action, a result page, an exports page, a CSV download action, a template form or model, or a controller for them in the app. The engine serves all of them.
|
|
296
|
+
- The app's own runner only starts a job, and the job only calls `Ixport.run_import`. Never write a job that reads the file, matches rows or saves records itself.
|
|
297
|
+
- When the app's own runner class is renamed, tell the developer that the class name `ixport-install` set must be changed in the same change.
|
|
298
|
+
- Never wrap `Ixport.run_import` in a rescue, a transaction or a retry of the app's own. It records its own failure, keeps the rows saved before it, and reports the error.
|
|
299
|
+
- Installing ixport, running the update generator for the export templates table, the background runner's column or the failed import's error column, subscribing an error service to Rails' error reporter, mounting the engine, the tables an import, its refused rows and export templates are stored in, file storage, sign-in, how the current person and account are found, the layout, the inline row limit, and choosing whether imports run within the request, through wallflower or through the app's own runner class are out of scope for this local and belong to `ixport-install`.
|