ntq_excelsior 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +44 -2
- data/lib/ntq_excelsior/importer.rb +171 -0
- data/lib/ntq_excelsior/version.rb +1 -1
- data/lib/ntq_excelsior.rb +1 -0
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f5bdfecae33f17efa14aaac26fcfd4d2ebdc9e0117e5dc924e10c1b36022970
|
4
|
+
data.tar.gz: 8771fc83edec065d107912d57e45e8982a804c303ccea0b035bf206f2feaec51
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af4d4102f6ab0ad3d2cd82e75c2cb98bd114b84b798b3736b8de5bb32029da9754e7f7b378533f8c849ad99c7339a5ea170f9e4f8127f0d97dbc0e72d0537cf1
|
7
|
+
data.tar.gz: b0e9d976d84e39e76f47e5c60ae21aa7194e54c66ab910cbc6d568d4d6d3773a4235edfdc8844310e1a4ebc37f1e361e3b241aea9bfe03e55bff7414835d88a6
|
data/README.md
CHANGED
@@ -70,11 +70,53 @@ class UserExporter < NtqExcelsior::Exporter
|
|
70
70
|
end
|
71
71
|
|
72
72
|
exporter = UserExporter.new(@users)
|
73
|
-
exporter.export
|
73
|
+
stream = exporter.export.to_stream.read
|
74
|
+
|
75
|
+
# In ruby file
|
74
76
|
File.open("export.xlsx", "w") do |tpm|
|
75
77
|
tpm.binmode
|
76
|
-
tpm.write(
|
78
|
+
tpm.write(stream)
|
77
79
|
end
|
80
|
+
|
81
|
+
# In Controller action
|
82
|
+
send_data stream, type: 'application/xlsx', filename: "filename.xlsx"
|
83
|
+
```
|
84
|
+
|
85
|
+
### Import
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class UserImporter < NtqExcelsior::Importer
|
89
|
+
|
90
|
+
model_klass "User"
|
91
|
+
|
92
|
+
primary_key :id
|
93
|
+
|
94
|
+
schema({
|
95
|
+
email: 'Email',
|
96
|
+
first_name: /Prénom/i,
|
97
|
+
last_name: {
|
98
|
+
header: /Nom/i,
|
99
|
+
required: true
|
100
|
+
},
|
101
|
+
active: {
|
102
|
+
header: /Actif/i,
|
103
|
+
required: false
|
104
|
+
}
|
105
|
+
})
|
106
|
+
|
107
|
+
def import_line(line, save: true)
|
108
|
+
super do |record, line|
|
109
|
+
record.email = line[:email]
|
110
|
+
record.first_name = line[:first_name]
|
111
|
+
record.last_name = line[:last_name]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
importer = UserImporter.new
|
117
|
+
importer.file = file
|
118
|
+
result = importer.import
|
119
|
+
# { success_count: 2, error_lines: [] }
|
78
120
|
```
|
79
121
|
|
80
122
|
## Development
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'roo'
|
2
|
+
|
3
|
+
module NtqExcelsior
|
4
|
+
class Importer
|
5
|
+
|
6
|
+
attr_accessor :file, :check, :lines, :options, :status_tracker
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def autosave(value = nil)
|
11
|
+
@autosave ||= value
|
12
|
+
end
|
13
|
+
|
14
|
+
def spreadsheet_options(value = nil)
|
15
|
+
@spreadsheet_options ||= value
|
16
|
+
end
|
17
|
+
|
18
|
+
def primary_key(value = nil)
|
19
|
+
@primary_key ||= value
|
20
|
+
end
|
21
|
+
|
22
|
+
def model_klass(value = nil)
|
23
|
+
@model_klass ||= value
|
24
|
+
end
|
25
|
+
|
26
|
+
def schema(value = nil)
|
27
|
+
@schema ||= value
|
28
|
+
end
|
29
|
+
|
30
|
+
def max_error_count(value = nil)
|
31
|
+
@max_error_count ||= value
|
32
|
+
end
|
33
|
+
|
34
|
+
def structure(value = nil)
|
35
|
+
@structure ||= value
|
36
|
+
end
|
37
|
+
|
38
|
+
def sample_file(value = nil)
|
39
|
+
@sample_file ||= value
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def spreadsheet
|
44
|
+
return @spreadsheet unless @spreadsheet.nil?
|
45
|
+
|
46
|
+
raise 'File is missing' unless file.present?
|
47
|
+
|
48
|
+
@spreadsheet = Roo::Spreadsheet.open(file, self.class.spreadsheet_options || {})
|
49
|
+
end
|
50
|
+
|
51
|
+
def required_headers
|
52
|
+
return @required_headers if @required_headers
|
53
|
+
|
54
|
+
@required_columns = self.class.schema.select { |field, column_config| !column_config.is_a?(Hash) || !column_config.has_key?(:required) || column_config[:required] }
|
55
|
+
@required_line_keys = @required_columns.map{ |k, v| k }
|
56
|
+
@required_headers = @required_columns.map{ |k, column_config| column_config.is_a?(Hash) ? column_config[:header] : column_config }.map{|header| header.is_a?(String) ? Regexp.new(header, "i") : header}
|
57
|
+
if self.class.primary_key && !@required_line_keys.include?(self.class.primary_key)
|
58
|
+
@required_line_keys = @required_line_keys.unshift(self.class.primary_key)
|
59
|
+
@required_headers = @required_headers.unshift(Regexp.new(self.class.primary_key.to_s, "i"))
|
60
|
+
end
|
61
|
+
@required_headers
|
62
|
+
end
|
63
|
+
|
64
|
+
def spreadsheet_data
|
65
|
+
spreadsheet.sheet(spreadsheet.sheets[0]).parse(header_search: required_headers)
|
66
|
+
end
|
67
|
+
|
68
|
+
def detect_header_scheme
|
69
|
+
return @header_scheme if @header_scheme
|
70
|
+
@header_scheme = {}
|
71
|
+
l = spreadsheet_data[0].dup
|
72
|
+
|
73
|
+
self.class.schema.each do |field, column_config|
|
74
|
+
header = column_config.is_a?(Hash) ? column_config[:header] : column_config
|
75
|
+
|
76
|
+
l.each do |parsed_header, _value|
|
77
|
+
next unless header.is_a?(Regexp) && parsed_header.match?(header) || header.is_a?(String) && parsed_header == header
|
78
|
+
|
79
|
+
l.delete(parsed_header)
|
80
|
+
@header_scheme[parsed_header] = field
|
81
|
+
end
|
82
|
+
end
|
83
|
+
@header_scheme[self.class.primary_key.to_s] = self.class.primary_key.to_s if self.class.primary_key && !self.class.schema[self.class.primary_key.to_sym]
|
84
|
+
|
85
|
+
@header_scheme
|
86
|
+
end
|
87
|
+
|
88
|
+
def parse_line(line)
|
89
|
+
parsed_line = {}
|
90
|
+
line.each do |header, value|
|
91
|
+
header_scheme = detect_header_scheme
|
92
|
+
if header.to_s == self.class.primary_key.to_s
|
93
|
+
parsed_line[self.class.primary_key] = value
|
94
|
+
next
|
95
|
+
end
|
96
|
+
|
97
|
+
header_scheme.each do |header, field|
|
98
|
+
parsed_line[field.to_sym] = line[header]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
raise Roo::HeaderRowNotFoundError unless (@required_line_keys - parsed_line.keys).size == 0
|
103
|
+
|
104
|
+
parsed_line
|
105
|
+
end
|
106
|
+
|
107
|
+
def lines
|
108
|
+
return @lines if @lines
|
109
|
+
|
110
|
+
@lines = spreadsheet_data.map {|line| parse_line(line) }
|
111
|
+
end
|
112
|
+
|
113
|
+
# id for default query in model
|
114
|
+
# line in case an override is needed to find correct record
|
115
|
+
def find_or_initialize_record(line)
|
116
|
+
return nil unless self.class.primary_key && self.class.model_klass
|
117
|
+
|
118
|
+
self.class.model_klass.constantize.find_or_initialize_by("#{self.class.primary_key}": line[self.class.primary_key.to_sym])
|
119
|
+
end
|
120
|
+
|
121
|
+
def import_line(line, save: true)
|
122
|
+
record = find_or_initialize_record(line)
|
123
|
+
@success = false
|
124
|
+
@action = nil
|
125
|
+
@errors = []
|
126
|
+
|
127
|
+
yield(record, line) if block_given?
|
128
|
+
|
129
|
+
if (self.class.autosave.nil? || self.class.autosave)
|
130
|
+
@action = record.persisted? ? 'update' : 'create'
|
131
|
+
if save
|
132
|
+
@success = record.save
|
133
|
+
else
|
134
|
+
@success = record.valid?
|
135
|
+
end
|
136
|
+
@errors = record.errors.full_messages.concat(@errors) if record.errors.any?
|
137
|
+
end
|
138
|
+
|
139
|
+
return { status: :success, action: @action } if @success
|
140
|
+
|
141
|
+
return { status: :error, errors: @errors.join(", ") }
|
142
|
+
end
|
143
|
+
|
144
|
+
def import(save: true, status_tracker: nil)
|
145
|
+
at = 0
|
146
|
+
errors_lines = []
|
147
|
+
success_count = 0
|
148
|
+
lines.each_with_index do |line, index|
|
149
|
+
break if errors_lines.size == self.class.max_error_count
|
150
|
+
|
151
|
+
result = import_line(line.with_indifferent_access, save: true)
|
152
|
+
case result[:status]
|
153
|
+
when :success
|
154
|
+
success_count += 1
|
155
|
+
when :error
|
156
|
+
error_line = line.map { |k, v| v }
|
157
|
+
error_line << result[:errors]
|
158
|
+
errors_lines.push(error_line)
|
159
|
+
end
|
160
|
+
|
161
|
+
if @status_tracker&.is_a?(Proc)
|
162
|
+
at = (((index + 1).to_d / lines.size) * 100.to_d)
|
163
|
+
@status_tracker.call(at)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
{ success_count: success_count, errors: errors_lines }
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
data/lib/ntq_excelsior.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ntq_excelsior
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-02-
|
11
|
+
date: 2023-02-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: caxlsx
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "<"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: roo
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "<"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "<"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3'
|
27
41
|
description: Library use by 9tq for import/export
|
28
42
|
email:
|
29
43
|
- kevin@9troisquarts.com
|
@@ -41,6 +55,7 @@ files:
|
|
41
55
|
- Rakefile
|
42
56
|
- lib/ntq_excelsior.rb
|
43
57
|
- lib/ntq_excelsior/exporter.rb
|
58
|
+
- lib/ntq_excelsior/importer.rb
|
44
59
|
- lib/ntq_excelsior/version.rb
|
45
60
|
- sig/ntq_excelsior.rbs
|
46
61
|
homepage: https://github.com/9troisquarts/ntq-excelsior
|