card-mod-csv_import 0.8
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 +7 -0
- data/config/initializers/import_logger.rb +2 -0
- data/lib/csv_file.rb +137 -0
- data/lib/csv_row.rb +167 -0
- data/lib/csv_row/normalizer.rb +12 -0
- data/lib/import_log.rb +7 -0
- data/lib/import_manager.rb +73 -0
- data/lib/import_manager/act_import_manager.rb +43 -0
- data/lib/import_manager/conflicts.rb +50 -0
- data/lib/import_manager/script_import_manager.rb +48 -0
- data/lib/import_manager/status.rb +50 -0
- data/lib/import_manager/status/counts.rb +31 -0
- data/lib/import_manager/status_log.rb +80 -0
- data/lib/import_manager/validation_manager.rb +40 -0
- data/set/abstract/import.rb +89 -0
- data/set/abstract/import/01_table_row.rb +112 -0
- data/set/abstract/import/execute_import.rb +59 -0
- data/set/abstract/import/import_page.rb +82 -0
- data/set/abstract/import/table.rb +76 -0
- data/set/all/import_act.rb +8 -0
- data/set/right/import_status.rb +163 -0
- data/set/right/imported_rows.rb +20 -0
- data/set/type/file.rb +10 -0
- data/template/self/import_tool.haml +3 -0
- metadata +83 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
event :import_csv, :integrate_with_delay, on: :update, when: :data_import? do
|
2
|
+
import_manager.import_rows selected_row_indices
|
3
|
+
end
|
4
|
+
|
5
|
+
event :flag_as_import, :prepare_to_validate, on: :update, when: :data_import? do
|
6
|
+
@empty_ok = true
|
7
|
+
end
|
8
|
+
|
9
|
+
event :prepare_import, :prepare_to_store, on: :update, when: :data_import? do
|
10
|
+
import_status_card.reset selected_row_count
|
11
|
+
end
|
12
|
+
|
13
|
+
def import_manager
|
14
|
+
@import_mananger =
|
15
|
+
::ActImportManager.new self, csv_file, conflict_strategy, extra_data
|
16
|
+
end
|
17
|
+
|
18
|
+
def conflict_strategy
|
19
|
+
Env.params[:conflict_strategy]&.to_sym || :skip
|
20
|
+
end
|
21
|
+
|
22
|
+
def extra_data
|
23
|
+
@extra_data ||= normalize_extra_data
|
24
|
+
end
|
25
|
+
|
26
|
+
def normalize_extra_data
|
27
|
+
fetch_hash_from_params(:extra_data).deep_symbolize_keys
|
28
|
+
end
|
29
|
+
|
30
|
+
def fetch_hash_from_params key
|
31
|
+
case Env.params[key]
|
32
|
+
when Hash
|
33
|
+
Env.params[key]
|
34
|
+
when ActionController::Parameters
|
35
|
+
Env.params[key].to_unsafe_h
|
36
|
+
else
|
37
|
+
{}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def data_import?
|
42
|
+
Env.params[:import_rows].present?
|
43
|
+
end
|
44
|
+
|
45
|
+
def silent_change?
|
46
|
+
data_import? || super
|
47
|
+
end
|
48
|
+
|
49
|
+
def selected_row_count
|
50
|
+
selected_row_indices.size
|
51
|
+
end
|
52
|
+
|
53
|
+
def selected_row_indices
|
54
|
+
@selected_row_indices ||=
|
55
|
+
fetch_hash_from_params(:import_rows).each_with_object([]) do |(index, value), a|
|
56
|
+
next unless [true, "true"].include?(value)
|
57
|
+
a << index.to_i
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
format :html do
|
2
|
+
def humanized_attachment_name
|
3
|
+
"csv file"
|
4
|
+
end
|
5
|
+
|
6
|
+
view :import, cache: :never do
|
7
|
+
class_up "card-slot", "nodblclick"
|
8
|
+
frame_and_form :update do
|
9
|
+
[
|
10
|
+
hidden_import_tags,
|
11
|
+
render!(:additional_form_fields),
|
12
|
+
render!(:import_table_helper),
|
13
|
+
render!(:import_table),
|
14
|
+
render!(:import_button_formgroup)
|
15
|
+
]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def hidden_import_tags
|
20
|
+
hidden_tags success: { name: card.import_status_card.name, view: :open }
|
21
|
+
end
|
22
|
+
|
23
|
+
view :import_button_formgroup do
|
24
|
+
button_formgroup { [import_button, cancel_button(href: path)] }
|
25
|
+
end
|
26
|
+
|
27
|
+
def import_button
|
28
|
+
button_tag "Import", class: "submit-button",
|
29
|
+
data: { disable_with: "Importing" }
|
30
|
+
end
|
31
|
+
|
32
|
+
view :import_table_helper do
|
33
|
+
wrap_with(:p, group_selection_checkboxes) +
|
34
|
+
wrap_with(:p, select_conflict_strategy)
|
35
|
+
end
|
36
|
+
|
37
|
+
view :additional_form_fields do
|
38
|
+
""
|
39
|
+
end
|
40
|
+
|
41
|
+
def already_imported?
|
42
|
+
card.already_imported?
|
43
|
+
end
|
44
|
+
|
45
|
+
def group_selection_checkboxes
|
46
|
+
<<-HTML.html_safe
|
47
|
+
Select:
|
48
|
+
<span class="padding-20 background-grey">
|
49
|
+
#{check_box_tag '_check-all', '', false, class: 'checkbox-button'}
|
50
|
+
#{label_tag '_check-all', 'all'}
|
51
|
+
</span>
|
52
|
+
#{group_selection_checkbox('exact', 'exact matches', :success, true)}
|
53
|
+
#{group_selection_checkbox('alias', 'alias matches', :alias, true)}
|
54
|
+
#{group_selection_checkbox('partial', 'partial matches', :info, true)}
|
55
|
+
#{group_selection_checkbox('none', 'no matches', :warning)}
|
56
|
+
#{group_selection_checkbox('imported', 'already imported', :active) if already_imported?}
|
57
|
+
HTML
|
58
|
+
end
|
59
|
+
|
60
|
+
def group_selection_checkbox name, label, identifier, checked=false
|
61
|
+
wrap_with :span, class: "padding-20 table-#{identifier}" do
|
62
|
+
[
|
63
|
+
check_box_tag(
|
64
|
+
name, "", checked,
|
65
|
+
class: "checkbox-button _group-check",
|
66
|
+
data: { group: name }
|
67
|
+
),
|
68
|
+
label_tag(name, label)
|
69
|
+
]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def select_conflict_strategy
|
74
|
+
<<-HTML.html_safe
|
75
|
+
Conflicts with existing entries:
|
76
|
+
#{radio_button_tag 'conflict_strategy', 'skip', true}
|
77
|
+
#{label_tag 'conflict_strategy_skip', 'skip'}
|
78
|
+
#{radio_button_tag 'conflict_strategy', 'override', false}
|
79
|
+
#{label_tag 'conflict_strategy_override', 'override'}
|
80
|
+
HTML
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
format :html do
|
2
|
+
def import_table_row_class
|
3
|
+
Abstract::Import::TableRow
|
4
|
+
end
|
5
|
+
|
6
|
+
def column_keys
|
7
|
+
card.singleton_class::COLUMNS.keys
|
8
|
+
end
|
9
|
+
|
10
|
+
def column_titles
|
11
|
+
card.singleton_class::COLUMNS.values
|
12
|
+
end
|
13
|
+
|
14
|
+
def row_errors
|
15
|
+
return "" unless vm.errors?
|
16
|
+
alert(:danger, true) do
|
17
|
+
with_header "Invalid data", level: 4 do
|
18
|
+
list_group vm.error_list
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def validation_manager
|
24
|
+
@vm ||= ValidationManager.new card.csv_file
|
25
|
+
end
|
26
|
+
|
27
|
+
alias_method :vm, :validation_manager
|
28
|
+
delegate :already_imported?, to: :card
|
29
|
+
|
30
|
+
def row_buckets
|
31
|
+
row_buckets = {
|
32
|
+
invalid: [],
|
33
|
+
valid: [],
|
34
|
+
imported: []
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def bucket_key table_row
|
39
|
+
if already_imported? table_row.row_index
|
40
|
+
:imported
|
41
|
+
elsif table_row.status == :failed
|
42
|
+
:invalid
|
43
|
+
else
|
44
|
+
:valid
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def table_rows
|
49
|
+
rb = row_buckets
|
50
|
+
vm.validate do |validated_csv_row|
|
51
|
+
table_row = import_table_row_class.new validated_csv_row, self
|
52
|
+
rb[bucket_key(table_row)] << table_row.render
|
53
|
+
end
|
54
|
+
rb.values.flatten
|
55
|
+
end
|
56
|
+
|
57
|
+
def table_with_errors *args
|
58
|
+
row_errors + table(*args)
|
59
|
+
end
|
60
|
+
|
61
|
+
view :import_table, cache: :never do
|
62
|
+
return alert(:warning) { "no import file attached" } if card.file.blank?
|
63
|
+
table_with_errors(table_rows, class: "_import-table import-table table-hover",
|
64
|
+
header: column_titles)
|
65
|
+
end
|
66
|
+
|
67
|
+
def extra_data_input_name index, *subfields
|
68
|
+
name = "extra_data[#{index}]"
|
69
|
+
name << subfields.map { |f| "[#{f}]" }.join("") if subfields.present?
|
70
|
+
name
|
71
|
+
end
|
72
|
+
|
73
|
+
def corrections_input_name index, key
|
74
|
+
extra_data_input_name index, :corrections, key
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
def followable?
|
2
|
+
false
|
3
|
+
end
|
4
|
+
|
5
|
+
def history?
|
6
|
+
false
|
7
|
+
end
|
8
|
+
|
9
|
+
def status
|
10
|
+
@status ||= ImportManager::Status.new content
|
11
|
+
end
|
12
|
+
|
13
|
+
def import_counts
|
14
|
+
@import_counts ||= status[:counts]
|
15
|
+
end
|
16
|
+
|
17
|
+
def reset total
|
18
|
+
@status = ImportManager::Status.new act_id: ActManager.act&.id, counts: { total: total }
|
19
|
+
save_status
|
20
|
+
end
|
21
|
+
|
22
|
+
def step key
|
23
|
+
import_counts.step key
|
24
|
+
save_status
|
25
|
+
end
|
26
|
+
|
27
|
+
def save_status
|
28
|
+
update_attributes content: status.to_json
|
29
|
+
end
|
30
|
+
|
31
|
+
STATUS_HEADER = {
|
32
|
+
failed: "Failed",
|
33
|
+
imported: "Successfully created",
|
34
|
+
overridden: "Overridden",
|
35
|
+
skipped: "Skipped existing"
|
36
|
+
}.freeze
|
37
|
+
|
38
|
+
STATUS_CONTEXT = {
|
39
|
+
failed: :danger,
|
40
|
+
imported: :success,
|
41
|
+
overridden: :warning,
|
42
|
+
skipped: :info
|
43
|
+
}.freeze
|
44
|
+
|
45
|
+
format :html do
|
46
|
+
delegate :status, :import_counts, to: :card
|
47
|
+
delegate :percentage, :count, :step, to: :import_counts
|
48
|
+
|
49
|
+
def wrap_data _slot=true
|
50
|
+
super.merge "refresh-url" => path(view: @slot_view)
|
51
|
+
end
|
52
|
+
|
53
|
+
def wrap_classes slot
|
54
|
+
class_up "card-slot", "_refresh-timer", true if auto_refresh?
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
def auto_refresh?
|
59
|
+
@slot_view.in?([:open, :content, :titled]) && importing?
|
60
|
+
end
|
61
|
+
|
62
|
+
def importing?
|
63
|
+
STATUS_CONTEXT.keys.inject(0) do |sum, key|
|
64
|
+
sum + count(key)
|
65
|
+
end < count(:total)
|
66
|
+
end
|
67
|
+
|
68
|
+
# returns plural if there are more than one card of type `count_type`
|
69
|
+
def item_label count_type=nil
|
70
|
+
label = card.left&.try(:item_label) || "card"
|
71
|
+
count_type && count(count_type) > 1 ? label.pluralize : label
|
72
|
+
end
|
73
|
+
|
74
|
+
def item_count_label count_key
|
75
|
+
label = item_label count_key
|
76
|
+
"#{count(count_key)} #{label}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def progress_header
|
80
|
+
if importing?
|
81
|
+
"Importing #{item_count_label :total} ..."
|
82
|
+
elsif count(:overridden).positive?
|
83
|
+
"#{item_count_label :imported} created and " \
|
84
|
+
"#{item_count_label :overridden} updated" \
|
85
|
+
else
|
86
|
+
"Imported #{item_count_label(:imported)}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
view :core, cache: :never do
|
91
|
+
with_header(progress_header, level: 4) do
|
92
|
+
_render_progress_bar
|
93
|
+
end + wrap_with(:p, undo_button) + wrap_with(:p, report)
|
94
|
+
end
|
95
|
+
|
96
|
+
view :progress_bar, cache: :never do
|
97
|
+
sections = %i[imported skipped overridden failed].map do |type|
|
98
|
+
progress_section type
|
99
|
+
end.compact
|
100
|
+
progress_bar(*sections)
|
101
|
+
end
|
102
|
+
|
103
|
+
view :compact, cache: :never, template: :haml do
|
104
|
+
end
|
105
|
+
|
106
|
+
def report
|
107
|
+
[:failed, :skipped, :overridden, :imported].map do |key|
|
108
|
+
next unless status[key].present?
|
109
|
+
generate_report_alert key
|
110
|
+
end.compact.join
|
111
|
+
end
|
112
|
+
|
113
|
+
def generate_report_alert type
|
114
|
+
alert STATUS_CONTEXT[type], false, false, href: "##{type}" do
|
115
|
+
with_header STATUS_HEADER[type], level: 5 do
|
116
|
+
list = []
|
117
|
+
status[type].each do |index, name|
|
118
|
+
list << report_line(index, name, type)
|
119
|
+
end
|
120
|
+
list_group list
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def report_line index, name, type
|
126
|
+
label, status_key =
|
127
|
+
type == :failed ? [name, :errors] : [link_to_card(name), :reports]
|
128
|
+
|
129
|
+
text = "##{index + 1}: #{label}"
|
130
|
+
if status[status_key][index].present?
|
131
|
+
text += " - " if name.present?
|
132
|
+
text += status[status_key][index].join("; ")
|
133
|
+
end
|
134
|
+
text
|
135
|
+
end
|
136
|
+
|
137
|
+
def progress_section type
|
138
|
+
return if count(type).zero?
|
139
|
+
html_class = "bg-#{STATUS_CONTEXT[type]}"
|
140
|
+
html_class << " progress-bar-striped progress-bar-animated" if importing?
|
141
|
+
{ value: percentage(type), label: "#{count(type)} #{type}", class: html_class }
|
142
|
+
end
|
143
|
+
|
144
|
+
def undo_button
|
145
|
+
return if importing?
|
146
|
+
return "" unless status[:act_id] && (act = Card::Act.find(status[:act_id]))
|
147
|
+
wrap_with :div, class: "d-flex flex-row-reverse" do
|
148
|
+
card.left.format(:html)
|
149
|
+
.revert_actions_link act, "Undo",
|
150
|
+
revert_to: :previous,
|
151
|
+
html_args: { class: "btn btn-danger",
|
152
|
+
"data-confirm" => undo_confirm_message }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def undo_confirm_message
|
157
|
+
text = "Do you really want to remove the imported #{item_label :imported}"
|
158
|
+
if count(:overridden).positive?
|
159
|
+
text += " and restore the overridden " + item_label(:overridden)
|
160
|
+
end
|
161
|
+
text << "?"
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
def followable?
|
2
|
+
false
|
3
|
+
end
|
4
|
+
|
5
|
+
def history?
|
6
|
+
false
|
7
|
+
end
|
8
|
+
|
9
|
+
def already_imported? index=nil
|
10
|
+
index ? imported_row_indices.include?(index) : imported_row_indices.present?
|
11
|
+
end
|
12
|
+
|
13
|
+
def mark_as_imported row_index
|
14
|
+
imported_rows_indices << row_index
|
15
|
+
update_attributes content: imported_row_indices.join(",")
|
16
|
+
end
|
17
|
+
|
18
|
+
def imported_row_indices
|
19
|
+
@imported_rows ||= ::Set.new content.split(",").map(&:to_i)
|
20
|
+
end
|
data/set/type/file.rb
ADDED
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: card-mod-csv_import
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.8'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Philipp Kühl
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-08-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: card
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: a card mod for importing data from csv files as cards into a deck
|
28
|
+
email:
|
29
|
+
- info@decko.org
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- config/initializers/import_logger.rb
|
35
|
+
- lib/csv_file.rb
|
36
|
+
- lib/csv_row.rb
|
37
|
+
- lib/csv_row/normalizer.rb
|
38
|
+
- lib/import_log.rb
|
39
|
+
- lib/import_manager.rb
|
40
|
+
- lib/import_manager/act_import_manager.rb
|
41
|
+
- lib/import_manager/conflicts.rb
|
42
|
+
- lib/import_manager/script_import_manager.rb
|
43
|
+
- lib/import_manager/status.rb
|
44
|
+
- lib/import_manager/status/counts.rb
|
45
|
+
- lib/import_manager/status_log.rb
|
46
|
+
- lib/import_manager/validation_manager.rb
|
47
|
+
- set/abstract/import.rb
|
48
|
+
- set/abstract/import/01_table_row.rb
|
49
|
+
- set/abstract/import/execute_import.rb
|
50
|
+
- set/abstract/import/import_page.rb
|
51
|
+
- set/abstract/import/table.rb
|
52
|
+
- set/all/import_act.rb
|
53
|
+
- set/right/import_status.rb
|
54
|
+
- set/right/imported_rows.rb
|
55
|
+
- set/type/file.rb
|
56
|
+
- template/self/import_tool.haml
|
57
|
+
homepage: http://decko.org
|
58
|
+
licenses:
|
59
|
+
- GPL-2.0
|
60
|
+
- GPL-3.0
|
61
|
+
metadata:
|
62
|
+
card-mod: csv_import
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: 2.3.0
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
requirements: []
|
78
|
+
rubyforge_project:
|
79
|
+
rubygems_version: 2.7.6
|
80
|
+
signing_key:
|
81
|
+
specification_version: 4
|
82
|
+
summary: tool to import csv files
|
83
|
+
test_files: []
|