data_porter 0.5.0 → 0.9.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/CHANGELOG.md +29 -0
- data/README.md +12 -6
- data/app/assets/javascripts/data_porter/import_form_controller.js +126 -0
- data/app/assets/stylesheets/data_porter/table.css +45 -0
- data/app/controllers/data_porter/concerns/import_validation.rb +47 -0
- data/app/controllers/data_porter/concerns/mapping_management.rb +43 -0
- data/app/controllers/data_porter/concerns/record_pagination.rb +19 -0
- data/app/controllers/data_porter/imports_controller.rb +14 -48
- data/app/views/data_porter/imports/index.html.erb +59 -115
- data/app/views/data_porter/imports/new.html.erb +51 -2
- data/app/views/data_porter/imports/show.html.erb +10 -0
- data/app/views/layouts/data_porter/application.html.erb +17 -146
- data/docs/CONFIGURATION.md +28 -6
- data/docs/ROADMAP.md +28 -0
- data/docs/TARGETS.md +54 -3
- data/lib/data_porter/components/shared/pagination.rb +53 -0
- data/lib/data_porter/components.rb +1 -0
- data/lib/data_porter/dsl/param.rb +32 -0
- data/lib/data_porter/engine.rb +4 -0
- data/lib/data_porter/orchestrator/dry_runner.rb +30 -0
- data/lib/data_porter/orchestrator/importer.rb +41 -0
- data/lib/data_porter/orchestrator/record_builder.rb +38 -0
- data/lib/data_porter/orchestrator.rb +16 -83
- data/lib/data_porter/registry.rb +21 -1
- data/lib/data_porter/target.rb +17 -1
- data/lib/data_porter/version.rb +1 -1
- metadata +14 -4
- /data/app/{javascript → assets/javascripts}/data_porter/mapping_controller.js +0 -0
- /data/app/{javascript → assets/javascripts}/data_porter/progress_controller.js +0 -0
- /data/app/{javascript → assets/javascripts}/data_porter/template_form_controller.js +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20e2b579cf4078a611095abd155090d7754e1e33e044a022136c846edb793bcb
|
|
4
|
+
data.tar.gz: 58e1bcad055c198aab6ccb71ddbbb5b219d5b82a7a94a34fb8c064f109d37878
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9929013331242330b53fe9f5e4b4940600a339c7e2d723551e864a969406cd2c516e70a10c95eff25c820b7f7e5904b63da4156b473d140e261d3a65ed9811eb
|
|
7
|
+
data.tar.gz: c9be1bbff65d0da5b36b5a837f590514ed29eb911a4ba527baa3e2f228f2d27252a4f4ff9bff3af579ac6674b6be19b32f0f4bf23aab992d42291877ea019e56
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.9.0] - 2026-02-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Import params DSL** -- Targets declare extra form fields via `params { param :hotel_id, type: :select, ... }` with support for `:select`, `:text`, `:number`, and `:hidden` types
|
|
13
|
+
- **`DSL::Param` struct** -- Stores param definitions (name, type, required, label, collection, default) with type validation
|
|
14
|
+
- **`import_params` accessor** -- Available in all target instance methods (`persist`, `transform`, `validate`, `after_import`, `on_error`), defaults to `{}`
|
|
15
|
+
- **Dynamic form rendering** -- Stimulus controller and inline JS dynamically render param fields when a target is selected, with default values and required indicators
|
|
16
|
+
- **Required params validation** -- Controller validates required import params on create, with error messages per missing field
|
|
17
|
+
- **Registry params serialization** -- `Registry.available` includes serialized param definitions with collection lambdas evaluated at call time
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Controller concerns extracted into `ImportValidation`, `MappingManagement`, `RecordPagination`
|
|
22
|
+
- Orchestrator logic extracted into `RecordBuilder`, `Importer`, `DryRunner` modules
|
|
23
|
+
- Inline JS in `new.html.erb` extracted for consistency
|
|
24
|
+
- 354 RSpec examples (up from 313), 0 failures
|
|
25
|
+
|
|
26
|
+
## [0.6.0] - 2026-02-07
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **Records pagination** -- Preview and completed pages paginate records at 50 per page with Previous/Next navigation
|
|
31
|
+
- **Pagination component** -- Phlex `Shared::Pagination` with disabled states on first/last page and anchor scrolling to keep viewport on the table
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- 313 RSpec examples (up from 300), 0 failures
|
|
36
|
+
|
|
8
37
|
## [0.5.0] - 2026-02-07
|
|
9
38
|
|
|
10
39
|
### Added
|
data/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# DataPorter
|
|
2
2
|
|
|
3
|
-
> [!
|
|
4
|
-
> This gem is under active development and not yet production-ready
|
|
3
|
+
> [!CAUTION]
|
|
4
|
+
> **This gem is under active development and not yet production-ready.**
|
|
5
|
+
> APIs and features may change without notice. Use at your own risk.
|
|
5
6
|
|
|
6
7
|
A mountable Rails engine for data import workflows: **Upload**, **Map**, **Preview**, **Import**.
|
|
7
8
|
|
|
@@ -23,9 +24,12 @@ Supports CSV, JSON, XLSX, and API sources with a declarative DSL for defining im
|
|
|
23
24
|
- **4 source types** -- CSV, XLSX, JSON, and API with a unified parsing pipeline
|
|
24
25
|
- **Interactive column mapping** -- Drag-free UI to match file headers to target fields ([docs](docs/MAPPING.md))
|
|
25
26
|
- **Mapping templates** -- Save and reuse column mappings across imports ([docs](docs/MAPPING.md#mapping-templates))
|
|
26
|
-
- **Real-time progress** --
|
|
27
|
+
- **Real-time progress** -- JSON polling with animated progress bar, no ActionCable required
|
|
27
28
|
- **Dry run mode** -- Validate against the database without persisting
|
|
28
29
|
- **Standalone UI** -- Self-contained layout with Turbo Drive and Stimulus, no host app dependencies
|
|
30
|
+
- **Import params** -- Declare extra form fields (select, text, number, hidden) per target for scoped imports ([docs](docs/TARGETS.md#params--))
|
|
31
|
+
- **Per-target source filtering** -- Each target declares its allowed sources, the UI filters accordingly
|
|
32
|
+
- **Import deletion & auto-purge** -- Delete imports from the UI, or schedule `rake data_porter:purge` for automatic cleanup
|
|
29
33
|
- **Declarative Target DSL** -- One class per import type, zero boilerplate ([docs](docs/TARGETS.md))
|
|
30
34
|
|
|
31
35
|
## Requirements
|
|
@@ -33,7 +37,6 @@ Supports CSV, JSON, XLSX, and API sources with a declarative DSL for defining im
|
|
|
33
37
|
- Ruby >= 3.2
|
|
34
38
|
- Rails >= 7.0
|
|
35
39
|
- ActiveStorage (for file uploads)
|
|
36
|
-
- ActionCable (optional, for real-time progress)
|
|
37
40
|
|
|
38
41
|
## Installation
|
|
39
42
|
|
|
@@ -54,7 +57,7 @@ The generator creates:
|
|
|
54
57
|
Generate a target:
|
|
55
58
|
|
|
56
59
|
```bash
|
|
57
|
-
bin/rails generate data_porter:target Product name:string:required price:integer sku:string
|
|
60
|
+
bin/rails generate data_porter:target Product name:string:required price:integer sku:string --sources csv xlsx
|
|
58
61
|
```
|
|
59
62
|
|
|
60
63
|
Implement `persist` in `app/importers/product_target.rb`:
|
|
@@ -110,6 +113,7 @@ pending -> parsing -> previewing -> importing -> completed
|
|
|
110
113
|
| [Targets](docs/TARGETS.md) | DSL reference, columns, hooks, generator |
|
|
111
114
|
| [Sources](docs/SOURCES.md) | CSV, JSON, XLSX, API setup and examples |
|
|
112
115
|
| [Column Mapping](docs/MAPPING.md) | Interactive mapping, templates, priority order |
|
|
116
|
+
| [Roadmap](docs/ROADMAP.md) | v1.0 plan and progress |
|
|
113
117
|
|
|
114
118
|
## Routes
|
|
115
119
|
|
|
@@ -118,6 +122,8 @@ pending -> parsing -> previewing -> importing -> completed
|
|
|
118
122
|
| GET | `/imports` | List imports |
|
|
119
123
|
| POST | `/imports` | Create import |
|
|
120
124
|
| GET | `/imports/:id` | Show import |
|
|
125
|
+
| DELETE | `/imports/:id` | Delete import |
|
|
126
|
+
| GET | `/imports/:id/status` | JSON progress polling |
|
|
121
127
|
| PATCH | `/imports/:id/update_mapping` | Save column mapping |
|
|
122
128
|
| POST | `/imports/:id/parse` | Parse source |
|
|
123
129
|
| POST | `/imports/:id/confirm` | Run import |
|
|
@@ -131,7 +137,7 @@ pending -> parsing -> previewing -> importing -> completed
|
|
|
131
137
|
git clone https://github.com/SerylLns/data_porter.git
|
|
132
138
|
cd data_porter
|
|
133
139
|
bin/setup
|
|
134
|
-
bundle exec rspec #
|
|
140
|
+
bundle exec rspec # 354 specs
|
|
135
141
|
bundle exec rubocop # 0 offenses
|
|
136
142
|
```
|
|
137
143
|
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = ["targetSelect", "sourceSelect", "fileField", "fileInput", "dropzone", "fileName", "modal", "paramsContainer"]
|
|
5
|
+
static values = { sources: Object, params: Object }
|
|
6
|
+
|
|
7
|
+
connect() {
|
|
8
|
+
this.filterSources()
|
|
9
|
+
this.renderParams()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
filterSources() {
|
|
13
|
+
if (!this.hasSourceSelectTarget || !this.hasTargetSelectTarget) return
|
|
14
|
+
var allowed = this.sourcesValue[this.targetSelectTarget.value]
|
|
15
|
+
var options = this.sourceSelectTarget.options
|
|
16
|
+
for (var i = 1; i < options.length; i++) {
|
|
17
|
+
options[i].style.display = allowed && allowed.indexOf(options[i].value) === -1 ? "none" : ""
|
|
18
|
+
}
|
|
19
|
+
if (allowed && this.sourceSelectTarget.selectedIndex > 0 && allowed.indexOf(this.sourceSelectTarget.value) === -1) {
|
|
20
|
+
this.sourceSelectTarget.selectedIndex = 0
|
|
21
|
+
this.fileFieldTarget.style.display = ""
|
|
22
|
+
}
|
|
23
|
+
this.renderParams()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
renderParams() {
|
|
27
|
+
if (!this.hasParamsContainerTarget || !this.hasTargetSelectTarget) return
|
|
28
|
+
this.paramsContainerTarget.innerHTML = ""
|
|
29
|
+
var defs = this.paramsValue[this.targetSelectTarget.value] || []
|
|
30
|
+
var self = this
|
|
31
|
+
defs.forEach(function(p) { self.paramsContainerTarget.appendChild(self.buildParamField(p)) })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
buildParamField(p) {
|
|
35
|
+
var div = document.createElement("div")
|
|
36
|
+
div.className = "dp-field"
|
|
37
|
+
div.appendChild(this.buildLabel(p))
|
|
38
|
+
div.appendChild(this.buildInput(p))
|
|
39
|
+
return div
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
buildLabel(p) {
|
|
43
|
+
var label = document.createElement("label")
|
|
44
|
+
label.className = "dp-label"
|
|
45
|
+
label.textContent = p.label + (p.required ? " *" : "")
|
|
46
|
+
return label
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
buildInput(p) {
|
|
50
|
+
if (p.type === "select" && p.collection) return this.buildSelect(p)
|
|
51
|
+
return this.buildTextField(p)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
buildSelect(p) {
|
|
55
|
+
var select = document.createElement("select")
|
|
56
|
+
select.className = "dp-select"
|
|
57
|
+
select.name = "data_import[config][import_params][" + p.name + "]"
|
|
58
|
+
if (p.required) select.required = true
|
|
59
|
+
var blank = document.createElement("option")
|
|
60
|
+
blank.value = ""
|
|
61
|
+
blank.textContent = "Select..."
|
|
62
|
+
select.appendChild(blank)
|
|
63
|
+
p.collection.forEach(function(opt) {
|
|
64
|
+
var o = document.createElement("option")
|
|
65
|
+
o.textContent = opt[0]
|
|
66
|
+
o.value = opt[1]
|
|
67
|
+
if (p["default"] && String(opt[1]) === String(p["default"])) o.selected = true
|
|
68
|
+
select.appendChild(o)
|
|
69
|
+
})
|
|
70
|
+
return select
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
buildTextField(p) {
|
|
74
|
+
var input = document.createElement("input")
|
|
75
|
+
input.className = "dp-input"
|
|
76
|
+
input.type = p.type === "number" ? "number" : (p.type === "hidden" ? "hidden" : "text")
|
|
77
|
+
input.name = "data_import[config][import_params][" + p.name + "]"
|
|
78
|
+
if (p["default"]) input.value = p["default"]
|
|
79
|
+
if (p.required) input.required = true
|
|
80
|
+
return input
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
toggleFileField() {
|
|
84
|
+
this.fileFieldTarget.style.display = this.sourceSelectTarget.value === "api" ? "none" : ""
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
handleFile() {
|
|
88
|
+
if (this.fileInputTarget.files.length > 0) {
|
|
89
|
+
this.fileNameTarget.textContent = this.fileInputTarget.files[0].name
|
|
90
|
+
this.fileNameTarget.style.display = ""
|
|
91
|
+
this.dropzoneTarget.classList.add("dp-dropzone--has-file")
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
dragover(event) {
|
|
96
|
+
event.preventDefault()
|
|
97
|
+
this.dropzoneTarget.classList.add("dp-dropzone--dragover")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
dragleave() {
|
|
101
|
+
this.dropzoneTarget.classList.remove("dp-dropzone--dragover")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
drop(event) {
|
|
105
|
+
event.preventDefault()
|
|
106
|
+
this.dropzoneTarget.classList.remove("dp-dropzone--dragover")
|
|
107
|
+
if (event.dataTransfer.files.length > 0) {
|
|
108
|
+
this.fileInputTarget.files = event.dataTransfer.files
|
|
109
|
+
this.fileInputTarget.dispatchEvent(new Event("change"))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
closeModal(event) {
|
|
114
|
+
if (event.key === "Escape") {
|
|
115
|
+
this.modalTarget.classList.remove("dp-modal--open")
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
openModal() {
|
|
120
|
+
this.modalTarget.classList.add("dp-modal--open")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
closeModalClick() {
|
|
124
|
+
this.modalTarget.classList.remove("dp-modal--open")
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -49,3 +49,48 @@
|
|
|
49
49
|
align-items: center;
|
|
50
50
|
gap: 0.5rem;
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
.dp-pagination {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
gap: 1rem;
|
|
58
|
+
margin: 1.5rem 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.dp-pagination__btn {
|
|
62
|
+
display: inline-flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
padding: 0.5rem 1rem;
|
|
65
|
+
font-size: 0.875rem;
|
|
66
|
+
font-weight: 500;
|
|
67
|
+
color: var(--dp-primary);
|
|
68
|
+
background: white;
|
|
69
|
+
border: 1px solid var(--dp-gray-300);
|
|
70
|
+
border-radius: var(--dp-radius-md);
|
|
71
|
+
text-decoration: none;
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
transition: all 0.15s ease;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.dp-pagination__btn:hover {
|
|
77
|
+
background: var(--dp-gray-50);
|
|
78
|
+
border-color: var(--dp-primary);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.dp-pagination__btn--disabled {
|
|
82
|
+
color: var(--dp-gray-400);
|
|
83
|
+
cursor: default;
|
|
84
|
+
pointer-events: none;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.dp-pagination__btn--disabled:hover {
|
|
88
|
+
background: white;
|
|
89
|
+
border-color: var(--dp-gray-300);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.dp-pagination__info {
|
|
93
|
+
font-size: 0.875rem;
|
|
94
|
+
color: var(--dp-gray-500);
|
|
95
|
+
font-weight: 500;
|
|
96
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DataPorter
|
|
4
|
+
module Concerns
|
|
5
|
+
module ImportValidation
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def valid_source_for_target?
|
|
11
|
+
target = DataPorter::Registry.find(@import.target_key)
|
|
12
|
+
allowed = target._sources || DataPorter.configuration.enabled_sources
|
|
13
|
+
return true if allowed.map(&:to_s).include?(@import.source_type.to_s)
|
|
14
|
+
|
|
15
|
+
@import.errors.add(:source_type, "#{@import.source_type} is not available for this target")
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def valid_file_presence?
|
|
20
|
+
return true unless %w[csv json xlsx].include?(@import.source_type)
|
|
21
|
+
return true if @import.file.attached?
|
|
22
|
+
|
|
23
|
+
@import.errors.add(:file, "must be attached for #{@import.source_type.upcase} imports")
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def valid_import_params?
|
|
28
|
+
missing = missing_required_params
|
|
29
|
+
return true if missing.empty?
|
|
30
|
+
|
|
31
|
+
missing.each { |p| @import.errors.add(:base, "#{p.label} is required") }
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def missing_required_params
|
|
36
|
+
target = DataPorter::Registry.find(@import.target_key)
|
|
37
|
+
required = (target._params || []).select(&:required)
|
|
38
|
+
values = import_param_values
|
|
39
|
+
required.reject { |p| values[p.name.to_s].present? }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def import_param_values
|
|
43
|
+
(@import.config || {}).fetch("import_params", {})
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DataPorter
|
|
4
|
+
module Concerns
|
|
5
|
+
module MappingManagement
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def load_mapping_data
|
|
11
|
+
target = @import.target_class
|
|
12
|
+
columns = target._columns || []
|
|
13
|
+
@file_headers = @import.config["file_headers"] || []
|
|
14
|
+
@target_columns = columns.map { |c| [c.label, c.name.to_s, c.required] }
|
|
15
|
+
@default_mapping = (target._csv_mappings || {}).transform_values(&:to_s)
|
|
16
|
+
@templates = load_templates
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def load_templates
|
|
20
|
+
return [] unless defined?(DataPorter::MappingTemplate)
|
|
21
|
+
|
|
22
|
+
DataPorter::MappingTemplate.for_target(@import.target_key)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def save_column_mapping
|
|
26
|
+
mapping = params.require(:column_mapping).permit!.to_h
|
|
27
|
+
merged = (@import.config || {}).merge("column_mapping" => mapping)
|
|
28
|
+
@import.update!(config: merged, status: :pending)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def save_template_if_requested
|
|
32
|
+
return unless params[:save_template] == "1"
|
|
33
|
+
return unless defined?(DataPorter::MappingTemplate)
|
|
34
|
+
|
|
35
|
+
mapping = params.require(:column_mapping).permit!.to_h
|
|
36
|
+
DataPorter::MappingTemplate.find_or_initialize_by(
|
|
37
|
+
target_key: @import.target_key,
|
|
38
|
+
name: params[:template_name].presence || "Default"
|
|
39
|
+
).update!(mapping: mapping)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DataPorter
|
|
4
|
+
module Concerns
|
|
5
|
+
module RecordPagination
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def paginate_records
|
|
11
|
+
per_page = 50
|
|
12
|
+
@total_pages = (@records.size.to_f / per_page).ceil
|
|
13
|
+
@total_pages = 1 if @total_pages.zero?
|
|
14
|
+
@page = (params[:page] || 1).to_i.clamp(1, @total_pages)
|
|
15
|
+
@records = @records.slice((@page - 1) * per_page, per_page) || []
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module DataPorter
|
|
4
4
|
class ImportsController < DataPorter.configuration.parent_controller.constantize
|
|
5
|
+
include Concerns::ImportValidation
|
|
6
|
+
include Concerns::MappingManagement
|
|
7
|
+
include Concerns::RecordPagination
|
|
8
|
+
|
|
5
9
|
layout "data_porter/application"
|
|
6
10
|
|
|
7
11
|
before_action :set_import, only: %i[show parse confirm cancel dry_run update_mapping status destroy]
|
|
@@ -18,7 +22,7 @@ module DataPorter
|
|
|
18
22
|
def create
|
|
19
23
|
build_import
|
|
20
24
|
|
|
21
|
-
if valid_source_for_target? && valid_file_presence? && @import.save
|
|
25
|
+
if valid_source_for_target? && valid_file_presence? && valid_import_params? && @import.save
|
|
22
26
|
enqueue_after_create
|
|
23
27
|
redirect_to import_path(@import)
|
|
24
28
|
else
|
|
@@ -30,6 +34,7 @@ module DataPorter
|
|
|
30
34
|
@target = @import.target_class
|
|
31
35
|
@records = @import.records
|
|
32
36
|
@grouped = @records.group_by(&:status)
|
|
37
|
+
paginate_records
|
|
33
38
|
load_mapping_data if @import.mapping?
|
|
34
39
|
end
|
|
35
40
|
|
|
@@ -91,24 +96,17 @@ module DataPorter
|
|
|
91
96
|
end
|
|
92
97
|
|
|
93
98
|
def import_params
|
|
94
|
-
params.require(:data_import).permit(:target_key, :source_type, :file, config: {})
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def valid_source_for_target?
|
|
98
|
-
target = DataPorter::Registry.find(@import.target_key)
|
|
99
|
-
allowed = target._sources || DataPorter.configuration.enabled_sources
|
|
100
|
-
return true if allowed.map(&:to_s).include?(@import.source_type.to_s)
|
|
101
|
-
|
|
102
|
-
@import.errors.add(:source_type, "#{@import.source_type} is not available for this target")
|
|
103
|
-
false
|
|
99
|
+
permitted = params.require(:data_import).permit(:target_key, :source_type, :file, config: {})
|
|
100
|
+
merge_import_params(permitted)
|
|
104
101
|
end
|
|
105
102
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
return
|
|
103
|
+
def merge_import_params(permitted)
|
|
104
|
+
nested = params.dig(:data_import, :config, :import_params)
|
|
105
|
+
return permitted unless nested
|
|
109
106
|
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
config = permitted[:config] || {}
|
|
108
|
+
config["import_params"] = nested.permit!.to_h
|
|
109
|
+
permitted.merge(config: config)
|
|
112
110
|
end
|
|
113
111
|
|
|
114
112
|
def enqueue_after_create
|
|
@@ -118,37 +116,5 @@ module DataPorter
|
|
|
118
116
|
DataPorter::ParseJob.perform_later(@import.id)
|
|
119
117
|
end
|
|
120
118
|
end
|
|
121
|
-
|
|
122
|
-
def load_mapping_data
|
|
123
|
-
target = @import.target_class
|
|
124
|
-
columns = target._columns || []
|
|
125
|
-
@file_headers = @import.config["file_headers"] || []
|
|
126
|
-
@target_columns = columns.map { |c| [c.label, c.name.to_s, c.required] }
|
|
127
|
-
@default_mapping = (target._csv_mappings || {}).transform_values(&:to_s)
|
|
128
|
-
@templates = load_templates
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def load_templates
|
|
132
|
-
return [] unless defined?(DataPorter::MappingTemplate)
|
|
133
|
-
|
|
134
|
-
DataPorter::MappingTemplate.for_target(@import.target_key)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def save_column_mapping
|
|
138
|
-
mapping = params.require(:column_mapping).permit!.to_h
|
|
139
|
-
merged = (@import.config || {}).merge("column_mapping" => mapping)
|
|
140
|
-
@import.update!(config: merged, status: :pending)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def save_template_if_requested
|
|
144
|
-
return unless params[:save_template] == "1"
|
|
145
|
-
return unless defined?(DataPorter::MappingTemplate)
|
|
146
|
-
|
|
147
|
-
mapping = params.require(:column_mapping).permit!.to_h
|
|
148
|
-
DataPorter::MappingTemplate.find_or_initialize_by(
|
|
149
|
-
target_key: @import.target_key,
|
|
150
|
-
name: params[:template_name].presence || "Default"
|
|
151
|
-
).update!(mapping: mapping)
|
|
152
|
-
end
|
|
153
119
|
end
|
|
154
120
|
end
|