data_porter 2.9.0 → 2.10.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 +18 -0
- data/app/assets/javascripts/data_porter/import_form_controller.js +17 -2
- data/app/controllers/data_porter/concerns/import_params_builder.rb +43 -0
- data/app/controllers/data_porter/concerns/import_validation.rb +26 -0
- data/app/controllers/data_porter/imports_controller.rb +2 -22
- data/app/views/data_porter/imports/index.html.erb +8 -0
- data/app/views/data_porter/imports/new.html.erb +26 -1
- data/app/views/data_porter/imports/show.html.erb +8 -2
- data/config/locales/en.yml +3 -0
- data/config/locales/fr.yml +3 -0
- data/lib/data_porter/column_transformer.rb +2 -4
- data/lib/data_porter/components/preview/table.rb +1 -1
- data/lib/data_porter/orchestrator/record_builder.rb +1 -1
- data/lib/data_porter/record_revalidator.rb +3 -2
- data/lib/data_porter/record_updater.rb +2 -4
- data/lib/data_porter/record_validator.rb +1 -1
- data/lib/data_porter/registry.rb +12 -9
- data/lib/data_porter/sources/api.rb +1 -1
- data/lib/data_porter/sources/base.rb +3 -3
- data/lib/data_porter/sources/json.rb +1 -1
- data/lib/data_porter/store_models/import_record.rb +6 -2
- data/lib/data_porter/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb7f4978449eef549b221a5bb8c2e297ca19ce4a91161d80a18f02dce1e68555
|
|
4
|
+
data.tar.gz: 995a508847ef2ecf97796096a374e142026074662d349c9cb912272a4a6c0b0c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3660607affb95f6f4325f3175adf413c472a7fec30961919cc3138a7cdb5bea4ea94fe29ee894425ff596663ded515e9ba7d6d33a3c15ea97ccfccce4bbef8a1
|
|
7
|
+
data.tar.gz: 5691769ca62b86555a372ef942a0049b3373a7c2ed6585990189cc266a8047071ae13cd613189bb0bc6d07b7e939535cd7d82331bce7a1ca850ebb8da9a050b5
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.10.0] - 2026-10-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Header row field in import form** -- The modal and standalone `new` import forms show a "Header row" input for CSV and XLSX sources. Users enter a 1-based row number, stored as a 0-based `config["header_row"]`. The field is pre-filled from the Target's `header_row` DSL value, exposed by `Registry.available` as `header_row`
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **Import params on inline edit** -- `RecordUpdater` now hydrates `import_params` on the target before revalidating, so validations depending on them no longer fail on valid records (#3)
|
|
19
|
+
- **File extension validation** -- The uploaded file extension is cross-checked against the selected source type before saving, surfacing mismatches as a form error instead of crashing the parser (#4)
|
|
20
|
+
|
|
21
|
+
## [2.9.1] - 2026-03-11
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **Record data keys normalized to strings** -- `record.data` now uses `HashWithIndifferentAccess`, so both `record.data["key"]` and `record.data[:key]` work consistently. Sources, RecordBuilder, ColumnTransformer, and RecordValidator all produce string keys. No breaking change for existing Targets
|
|
26
|
+
- **Delete on mapping and preview** -- Imports in `mapping` and `previewing` status now show a Delete button with confirmation. Previously only `completed` and `failed` imports could be deleted. The preview Cancel button (which set status to `failed` without deleting) is replaced by Delete
|
|
27
|
+
|
|
10
28
|
## [2.9.0] - 2026-03-11
|
|
11
29
|
|
|
12
30
|
### Added
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
|
|
3
3
|
export default class extends Controller {
|
|
4
|
-
static targets = ["targetSelect", "sourceSelect", "fileField", "fileInput", "dropzone", "fileName", "modal", "paramsContainer"]
|
|
5
|
-
static values = { sources: Object, params: Object }
|
|
4
|
+
static targets = ["targetSelect", "sourceSelect", "fileField", "fileInput", "dropzone", "fileName", "modal", "paramsContainer", "headerRowField", "headerRowInput"]
|
|
5
|
+
static values = { sources: Object, params: Object, headerRows: Object }
|
|
6
6
|
|
|
7
7
|
connect() {
|
|
8
8
|
this.filterSources()
|
|
9
9
|
this.renderParams()
|
|
10
|
+
this.toggleHeaderRow()
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
filterSources() {
|
|
@@ -21,6 +22,19 @@ export default class extends Controller {
|
|
|
21
22
|
this.fileFieldTarget.style.display = ""
|
|
22
23
|
}
|
|
23
24
|
this.renderParams()
|
|
25
|
+
this.applyHeaderRowDefault()
|
|
26
|
+
this.toggleHeaderRow()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
applyHeaderRowDefault() {
|
|
30
|
+
if (!this.hasHeaderRowInputTarget || !this.hasTargetSelectTarget) return
|
|
31
|
+
this.headerRowInputTarget.value = this.headerRowsValue[this.targetSelectTarget.value] || 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
toggleHeaderRow() {
|
|
35
|
+
if (!this.hasHeaderRowFieldTarget || !this.hasSourceSelectTarget) return
|
|
36
|
+
var visible = ["csv", "xlsx"].indexOf(this.sourceSelectTarget.value) !== -1
|
|
37
|
+
this.headerRowFieldTarget.style.display = visible ? "" : "none"
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
renderParams() {
|
|
@@ -83,6 +97,7 @@ export default class extends Controller {
|
|
|
83
97
|
|
|
84
98
|
toggleFileField() {
|
|
85
99
|
this.fileFieldTarget.style.display = this.sourceSelectTarget.value === "api" ? "none" : ""
|
|
100
|
+
this.toggleHeaderRow()
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
handleFile() {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DataPorter
|
|
4
|
+
module Concerns
|
|
5
|
+
module ImportParamsBuilder
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def import_params
|
|
11
|
+
permitted = params.require(:data_import).permit(:target_key, :source_type, :file, config: {})
|
|
12
|
+
permitted = merge_import_params(permitted)
|
|
13
|
+
merge_header_row(permitted)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def merge_import_params(permitted)
|
|
17
|
+
nested = params.dig(:data_import, :config, :import_params)
|
|
18
|
+
return permitted unless nested
|
|
19
|
+
|
|
20
|
+
config = permitted[:config]&.to_unsafe_h || {}
|
|
21
|
+
config["import_params"] = nested.permit(*allowed_param_keys).to_h
|
|
22
|
+
permitted.merge(config: config)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def merge_header_row(permitted)
|
|
26
|
+
raw = params.dig(:data_import, :header_row)
|
|
27
|
+
return permitted if raw.blank?
|
|
28
|
+
|
|
29
|
+
config = permitted[:config]&.to_unsafe_h || {}
|
|
30
|
+
config["header_row"] = [raw.to_i - 1, 0].max
|
|
31
|
+
permitted.merge(config: config)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def allowed_param_keys
|
|
35
|
+
target_key = params.dig(:data_import, :target_key)
|
|
36
|
+
return [] unless target_key
|
|
37
|
+
|
|
38
|
+
target = DataPorter::Registry.find(target_key)
|
|
39
|
+
(target._params || []).map { |p| p.name.to_s }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -11,6 +11,12 @@ module DataPorter
|
|
|
11
11
|
"xlsx" => %w[application/vnd.openxmlformats-officedocument.spreadsheetml.sheet]
|
|
12
12
|
}.freeze
|
|
13
13
|
|
|
14
|
+
EXTENSIONS_BY_SOURCE = {
|
|
15
|
+
"csv" => %w[.csv],
|
|
16
|
+
"json" => %w[.json],
|
|
17
|
+
"xlsx" => %w[.xlsx]
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
14
20
|
private
|
|
15
21
|
|
|
16
22
|
def valid_source_for_target?
|
|
@@ -59,6 +65,26 @@ module DataPorter
|
|
|
59
65
|
false
|
|
60
66
|
end
|
|
61
67
|
|
|
68
|
+
def valid_file_extension_for_source?
|
|
69
|
+
return true unless @import.file.attached?
|
|
70
|
+
|
|
71
|
+
allowed = EXTENSIONS_BY_SOURCE[@import.source_type]
|
|
72
|
+
return true unless allowed
|
|
73
|
+
|
|
74
|
+
actual = File.extname(@import.file.blob.filename.to_s).downcase
|
|
75
|
+
return true if allowed.include?(actual)
|
|
76
|
+
|
|
77
|
+
@import.errors.add(
|
|
78
|
+
:file,
|
|
79
|
+
I18n.t(
|
|
80
|
+
"data_porter.errors.file_extension_mismatch",
|
|
81
|
+
expected: allowed.join(", "),
|
|
82
|
+
actual: actual.presence || "none"
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
|
|
62
88
|
def valid_file_content_type?
|
|
63
89
|
return true unless @import.file.attached?
|
|
64
90
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module DataPorter
|
|
4
4
|
class ImportsController < DataPorter.configuration.parent_controller.constantize
|
|
5
5
|
include Concerns::ImportValidation
|
|
6
|
+
include Concerns::ImportParamsBuilder
|
|
6
7
|
include Concerns::MappingManagement
|
|
7
8
|
include Concerns::RecordPagination
|
|
8
9
|
include Concerns::ScopeManagement
|
|
@@ -144,33 +145,12 @@ module DataPorter
|
|
|
144
145
|
valid_source_for_target?,
|
|
145
146
|
valid_file_presence?,
|
|
146
147
|
valid_file_size?,
|
|
148
|
+
valid_file_extension_for_source?,
|
|
147
149
|
valid_file_content_type?,
|
|
148
150
|
valid_import_params?
|
|
149
151
|
].all?
|
|
150
152
|
end
|
|
151
153
|
|
|
152
|
-
def import_params
|
|
153
|
-
permitted = params.require(:data_import).permit(:target_key, :source_type, :file, config: {})
|
|
154
|
-
merge_import_params(permitted)
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def merge_import_params(permitted)
|
|
158
|
-
nested = params.dig(:data_import, :config, :import_params)
|
|
159
|
-
return permitted unless nested
|
|
160
|
-
|
|
161
|
-
config = permitted[:config]&.to_unsafe_h || {}
|
|
162
|
-
config["import_params"] = nested.permit(*allowed_param_keys).to_h
|
|
163
|
-
permitted.merge(config: config)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def allowed_param_keys
|
|
167
|
-
target_key = params.dig(:data_import, :target_key)
|
|
168
|
-
return [] unless target_key
|
|
169
|
-
|
|
170
|
-
target = DataPorter::Registry.find(target_key)
|
|
171
|
-
(target._params || []).map { |p| p.name.to_s }
|
|
172
|
-
end
|
|
173
|
-
|
|
174
154
|
def enqueue_after_create
|
|
175
155
|
if @import.file_based?
|
|
176
156
|
DataPorter::ExtractHeadersJob.perform_later(@import.id)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<div class="data-porter" data-controller="data-porter--import-form"
|
|
2
2
|
data-data-porter--import-form-sources-value="<%= @targets.map { |t| [t[:key], t[:sources]] }.to_h.to_json %>"
|
|
3
3
|
data-data-porter--import-form-params-value="<%= @targets.map { |t| [t[:key], t[:params]] }.to_h.to_json %>"
|
|
4
|
+
data-data-porter--import-form-header-rows-value="<%= @targets.map { |t| [t[:key], t[:header_row]] }.to_h.to_json %>"
|
|
4
5
|
data-action="keydown@document->data-porter--import-form#closeModal">
|
|
5
6
|
<div class="dp-header">
|
|
6
7
|
<h1 class="dp-title"><%= t("data_porter.imports.title") %></h1>
|
|
@@ -105,6 +106,13 @@
|
|
|
105
106
|
</label>
|
|
106
107
|
</div>
|
|
107
108
|
|
|
109
|
+
<div class="dp-field" data-data-porter--import-form-target="headerRowField">
|
|
110
|
+
<%= label_tag "dp-header-row-input-modal", t("data_porter.imports.form.header_row_label"), class: "dp-label" %>
|
|
111
|
+
<input type="number" name="data_import[header_row]" id="dp-header-row-input-modal" class="dp-input" value="1" min="1" step="1"
|
|
112
|
+
data-data-porter--import-form-target="headerRowInput" />
|
|
113
|
+
<small class="dp-hint"><%= t("data_porter.imports.form.header_row_hint") %></small>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
108
116
|
<div class="dp-modal__footer">
|
|
109
117
|
<%= f.submit t("data_porter.imports.start_import"), class: "dp-btn dp-btn--primary" %>
|
|
110
118
|
<button type="button" class="dp-btn dp-btn--secondary" data-action="data-porter--import-form#closeModalClick"><%= t("data_porter.imports.cancel") %></button>
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
class: "dp-select",
|
|
22
22
|
data: {
|
|
23
23
|
sources: @targets.map { |t| [t[:key], t[:sources]] }.to_h.to_json,
|
|
24
|
-
params: @targets.map { |t| [t[:key], t[:params]] }.to_h.to_json
|
|
24
|
+
params: @targets.map { |t| [t[:key], t[:params]] }.to_h.to_json,
|
|
25
|
+
header_rows: @targets.map { |t| [t[:key], t[:header_row]] }.to_h.to_json
|
|
25
26
|
} %>
|
|
26
27
|
</div>
|
|
27
28
|
|
|
@@ -49,6 +50,12 @@
|
|
|
49
50
|
</label>
|
|
50
51
|
</div>
|
|
51
52
|
|
|
53
|
+
<div id="dp-header-row-field-new" class="dp-field">
|
|
54
|
+
<%= label_tag "dp-header-row-input-new", t("data_porter.imports.form.header_row_label"), class: "dp-label" %>
|
|
55
|
+
<input type="number" name="data_import[header_row]" id="dp-header-row-input-new" class="dp-input" value="1" min="1" step="1" />
|
|
56
|
+
<small class="dp-hint"><%= t("data_porter.imports.form.header_row_hint") %></small>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
52
59
|
<div class="dp-actions">
|
|
53
60
|
<%= f.submit t("data_porter.imports.start_import"), class: "dp-btn dp-btn--primary" %>
|
|
54
61
|
<%= link_to t("data_porter.imports.cancel"), imports_path, class: "dp-btn dp-btn--secondary" %>
|
|
@@ -64,8 +71,23 @@
|
|
|
64
71
|
var fileInput = document.getElementById("dp-file-input-new");
|
|
65
72
|
var dropzone = document.getElementById("dp-dropzone-new");
|
|
66
73
|
var fileName = document.getElementById("dp-file-name-new");
|
|
74
|
+
var headerRowField = document.getElementById("dp-header-row-field-new");
|
|
67
75
|
var paramsContainer = document.getElementById("dp-params-container");
|
|
68
76
|
var paramsData = JSON.parse((targetSelect && targetSelect.dataset.params) || "{}");
|
|
77
|
+
var HEADER_ROW_SOURCES = ["csv", "xlsx"];
|
|
78
|
+
|
|
79
|
+
var headerRowInput = document.getElementById("dp-header-row-input-new");
|
|
80
|
+
var headerRowsData = JSON.parse((targetSelect && targetSelect.dataset.headerRows) || "{}");
|
|
81
|
+
|
|
82
|
+
function toggleHeaderRow() {
|
|
83
|
+
if (!headerRowField || !sourceSelect) return;
|
|
84
|
+
headerRowField.style.display = HEADER_ROW_SOURCES.indexOf(sourceSelect.value) === -1 ? "none" : "";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function applyHeaderRowDefault() {
|
|
88
|
+
if (!headerRowInput || !targetSelect) return;
|
|
89
|
+
headerRowInput.value = headerRowsData[targetSelect.value] || 1;
|
|
90
|
+
}
|
|
69
91
|
|
|
70
92
|
function renderParams() {
|
|
71
93
|
if (!paramsContainer || !targetSelect) return;
|
|
@@ -124,13 +146,16 @@
|
|
|
124
146
|
targetSelect.addEventListener("change", function() {
|
|
125
147
|
filterSources();
|
|
126
148
|
renderParams();
|
|
149
|
+
applyHeaderRowDefault();
|
|
127
150
|
});
|
|
128
151
|
}
|
|
129
152
|
|
|
130
153
|
if (sourceSelect) {
|
|
131
154
|
sourceSelect.addEventListener("change", function() {
|
|
132
155
|
fileField.style.display = this.value === "api" ? "none" : "";
|
|
156
|
+
toggleHeaderRow();
|
|
133
157
|
});
|
|
158
|
+
toggleHeaderRow();
|
|
134
159
|
}
|
|
135
160
|
|
|
136
161
|
if (fileInput) {
|
|
@@ -42,6 +42,11 @@
|
|
|
42
42
|
templates: @templates,
|
|
43
43
|
default_mapping: @default_mapping
|
|
44
44
|
).call %>
|
|
45
|
+
<div class="dp-actions">
|
|
46
|
+
<%= button_to t("data_porter.imports.delete"), import_path(@import),
|
|
47
|
+
method: :delete, class: "dp-btn dp-btn--danger",
|
|
48
|
+
data: { turbo_confirm: t("data_porter.imports.delete_confirm") } %>
|
|
49
|
+
</div>
|
|
45
50
|
<% end %>
|
|
46
51
|
|
|
47
52
|
<% if @import.previewing? %>
|
|
@@ -71,8 +76,9 @@
|
|
|
71
76
|
<%= button_to t("data_porter.imports.back_to_mapping"), back_to_mapping_import_path(@import),
|
|
72
77
|
method: :post, class: "dp-btn dp-btn--secondary" %>
|
|
73
78
|
<% end %>
|
|
74
|
-
<%= button_to t("data_porter.imports.
|
|
75
|
-
method: :
|
|
79
|
+
<%= button_to t("data_porter.imports.delete"), import_path(@import),
|
|
80
|
+
method: :delete, class: "dp-btn dp-btn--danger",
|
|
81
|
+
data: { turbo_confirm: t("data_porter.imports.delete_confirm") } %>
|
|
76
82
|
</div>
|
|
77
83
|
<% end %>
|
|
78
84
|
|
data/config/locales/en.yml
CHANGED
|
@@ -43,6 +43,8 @@ en:
|
|
|
43
43
|
dropzone_text: "Drop your file here or <strong>browse</strong>"
|
|
44
44
|
dropzone_hint: "CSV, JSON, or XLSX files accepted"
|
|
45
45
|
select_prompt: "Select..."
|
|
46
|
+
header_row_label: "Header row"
|
|
47
|
+
header_row_hint: "Row number (starting at 1) containing the column headers. Leave at 1 unless your file has description rows above the headers."
|
|
46
48
|
mapping:
|
|
47
49
|
load_template: "Load Template"
|
|
48
50
|
save_as_template: "Save as template"
|
|
@@ -124,3 +126,4 @@ en:
|
|
|
124
126
|
file_required: "must be attached for %{source} imports"
|
|
125
127
|
file_too_large: "is too large (max %{max} MB)"
|
|
126
128
|
invalid_content_type: "has an invalid content type (%{type})"
|
|
129
|
+
file_extension_mismatch: "expected a %{expected} file but got %{actual}"
|
data/config/locales/fr.yml
CHANGED
|
@@ -43,6 +43,8 @@ fr:
|
|
|
43
43
|
dropzone_text: "Déposez votre fichier ici ou <strong>parcourir</strong>"
|
|
44
44
|
dropzone_hint: "Fichiers CSV, JSON ou XLSX acceptés"
|
|
45
45
|
select_prompt: "Sélectionner..."
|
|
46
|
+
header_row_label: "Ligne des en-têtes"
|
|
47
|
+
header_row_hint: "Numéro de la ligne (à partir de 1) contenant les en-têtes de colonnes. Laissez 1 sauf si votre fichier a des lignes de description avant les en-têtes."
|
|
46
48
|
mapping:
|
|
47
49
|
load_template: "Charger un modèle"
|
|
48
50
|
save_as_template: "Sauvegarder comme modèle"
|
|
@@ -124,3 +126,4 @@ fr:
|
|
|
124
126
|
file_required: "doit être joint pour les imports %{source}"
|
|
125
127
|
file_too_large: "est trop volumineux (max %{max} Mo)"
|
|
126
128
|
invalid_content_type: "a un type de contenu invalide (%{type})"
|
|
129
|
+
file_extension_mismatch: "fichier %{expected} attendu mais %{actual} reçu"
|
|
@@ -24,9 +24,10 @@ module DataPorter
|
|
|
24
24
|
def normalize_keys(record)
|
|
25
25
|
@columns.each do |col|
|
|
26
26
|
string_key = col.name.to_s
|
|
27
|
-
next
|
|
27
|
+
next if record.data.key?(string_key)
|
|
28
|
+
next unless record.data.key?(col.name)
|
|
28
29
|
|
|
29
|
-
record.data[
|
|
30
|
+
record.data[string_key] = record.data.delete(col.name)
|
|
30
31
|
end
|
|
31
32
|
end
|
|
32
33
|
end
|
|
@@ -5,6 +5,7 @@ module DataPorter
|
|
|
5
5
|
def initialize(data_import)
|
|
6
6
|
@data_import = data_import
|
|
7
7
|
@target = data_import.target_class.new
|
|
8
|
+
@target.import_params = data_import.config&.dig("import_params") || {}
|
|
8
9
|
@revalidator = RecordRevalidator.new(@target)
|
|
9
10
|
end
|
|
10
11
|
|
|
@@ -32,10 +33,7 @@ module DataPorter
|
|
|
32
33
|
record.data[key] = value
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
def resolve_key(
|
|
36
|
-
return column.to_sym if data.key?(column.to_sym)
|
|
37
|
-
return column.to_s if data.key?(column.to_s)
|
|
38
|
-
|
|
36
|
+
def resolve_key(_data, column)
|
|
39
37
|
column.to_s
|
|
40
38
|
end
|
|
41
39
|
|
data/lib/data_porter/registry.rb
CHANGED
|
@@ -16,15 +16,7 @@ module DataPorter
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def available
|
|
19
|
-
@targets.map
|
|
20
|
-
{
|
|
21
|
-
key: key,
|
|
22
|
-
label: klass._label,
|
|
23
|
-
icon: klass._icon,
|
|
24
|
-
sources: klass._sources || DataPorter.configuration.enabled_sources,
|
|
25
|
-
params: serialize_params(klass._params)
|
|
26
|
-
}
|
|
27
|
-
end
|
|
19
|
+
@targets.map { |key, klass| serialize_target(key, klass) }
|
|
28
20
|
end
|
|
29
21
|
|
|
30
22
|
def refresh!
|
|
@@ -37,6 +29,17 @@ module DataPorter
|
|
|
37
29
|
|
|
38
30
|
private
|
|
39
31
|
|
|
32
|
+
def serialize_target(key, klass)
|
|
33
|
+
{
|
|
34
|
+
key: key,
|
|
35
|
+
label: klass._label,
|
|
36
|
+
icon: klass._icon,
|
|
37
|
+
sources: klass._sources || DataPorter.configuration.enabled_sources,
|
|
38
|
+
params: serialize_params(klass._params),
|
|
39
|
+
header_row: (klass._header_row || 0) + 1
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
40
43
|
def resolve_collection(collection)
|
|
41
44
|
return unless collection
|
|
42
45
|
|
|
@@ -32,18 +32,18 @@ module DataPorter
|
|
|
32
32
|
|
|
33
33
|
def user_map(row)
|
|
34
34
|
user_mapping.each_with_object({}) do |(header, column), hash|
|
|
35
|
-
hash[column.
|
|
35
|
+
hash[column.to_s] = row[header]
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def explicit_map(row, mappings)
|
|
40
40
|
mappings.each_with_object({}) do |(header, column), hash|
|
|
41
|
-
hash[column] = row[header]
|
|
41
|
+
hash[column.to_s] = row[header]
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def auto_map(row)
|
|
46
|
-
row.to_h.transform_keys { |k| k.parameterize(separator: "_")
|
|
46
|
+
row.to_h.transform_keys { |k| k.parameterize(separator: "_") }
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def header_row_index
|
|
@@ -10,8 +10,12 @@ module DataPorter
|
|
|
10
10
|
|
|
11
11
|
attribute :line_number, :integer
|
|
12
12
|
attribute :status, :string, default: "pending"
|
|
13
|
-
attribute :data, default: -> {
|
|
13
|
+
attribute :data, default: -> { HashWithIndifferentAccess.new }
|
|
14
14
|
attribute :errors_list, Error.to_array_type, default: -> { [] }
|
|
15
|
+
|
|
16
|
+
def data=(value)
|
|
17
|
+
super(value.is_a?(Hash) ? value.with_indifferent_access : value)
|
|
18
|
+
end
|
|
15
19
|
attribute :warnings, Error.to_array_type, default: -> { [] }
|
|
16
20
|
attribute :target_id, :integer
|
|
17
21
|
attribute :dry_run_passed, :boolean, default: false
|
|
@@ -29,7 +33,7 @@ module DataPorter
|
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
def attributes
|
|
32
|
-
data.
|
|
36
|
+
data.stringify_keys.compact.with_indifferent_access
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
def determine_status!
|
data/lib/data_porter/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: data_porter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Seryl Lounis
|
|
@@ -123,6 +123,7 @@ files:
|
|
|
123
123
|
- app/assets/stylesheets/data_porter/progress.css
|
|
124
124
|
- app/assets/stylesheets/data_porter/table.css
|
|
125
125
|
- app/channels/data_porter/import_channel.rb
|
|
126
|
+
- app/controllers/data_porter/concerns/import_params_builder.rb
|
|
126
127
|
- app/controllers/data_porter/concerns/import_validation.rb
|
|
127
128
|
- app/controllers/data_porter/concerns/mapping_management.rb
|
|
128
129
|
- app/controllers/data_porter/concerns/record_pagination.rb
|