ntq_excelsior 0.2.0 → 0.4.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 +45 -4
- data/lib/ntq_excelsior/exporter.rb +34 -16
- data/lib/ntq_excelsior/importer.rb +155 -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: 4443b707f72d72748af1e8d5c728aef25ea2f7175704f653bebf2b1897ea1784
|
4
|
+
data.tar.gz: 7fdc4700b25c1d5773638186fb5e22f2afd9fa956cf755a5f4c6eccf4fb4f5d3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eb6b8a3fbe5ccf2ef25eb5ae3156e40c33fe8d3d67dda073b44a986fe0d4003e6a13ccd871868f2823259e06bd3b89a8c05fac433410927bdcd4337110aacdda
|
7
|
+
data.tar.gz: d0e1c3218ca3f1d85d627a35e594772df18cbe87ba7d6af5e37ad4932c4bb2acfa5823896e6e62457286b9125aefa57435bcdb0f2d450e7ca7a494f68df74b48
|
data/README.md
CHANGED
@@ -49,8 +49,7 @@ class UserExporter < NtqExcelsior::Exporter
|
|
49
49
|
},
|
50
50
|
{
|
51
51
|
title: 'Birthdate',
|
52
|
-
resolve: 'birthdate'
|
53
|
-
type: :date
|
52
|
+
resolve: 'birthdate'
|
54
53
|
}
|
55
54
|
{
|
56
55
|
title: 'Address (nested)',
|
@@ -59,6 +58,11 @@ class UserExporter < NtqExcelsior::Exporter
|
|
59
58
|
{
|
60
59
|
title: 'City (nested)',
|
61
60
|
resolve: ['address', 'city']
|
61
|
+
},
|
62
|
+
{
|
63
|
+
title: 'Age',
|
64
|
+
resolve: 'age',
|
65
|
+
type: :number
|
62
66
|
}
|
63
67
|
]
|
64
68
|
})
|
@@ -66,10 +70,47 @@ class UserExporter < NtqExcelsior::Exporter
|
|
66
70
|
end
|
67
71
|
|
68
72
|
exporter = UserExporter.new(@users)
|
69
|
-
exporter.export
|
73
|
+
stream = exporter.export.to_stream.read
|
74
|
+
|
75
|
+
# In ruby file
|
70
76
|
File.open("export.xlsx", "w") do |tpm|
|
71
77
|
tpm.binmode
|
72
|
-
tpm.write(
|
78
|
+
tpm.write(stream)
|
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
|
73
114
|
end
|
74
115
|
```
|
75
116
|
|
@@ -5,6 +5,12 @@ module NtqExcelsior
|
|
5
5
|
attr_accessor :data
|
6
6
|
|
7
7
|
DEFAULT_STYLES = {
|
8
|
+
date_format: {
|
9
|
+
format_code: 'dd-mm-yyyy'
|
10
|
+
},
|
11
|
+
time_format: {
|
12
|
+
format_code: 'dd-mm-yyyy hh:mm:ss'
|
13
|
+
},
|
8
14
|
bold: {
|
9
15
|
b: true
|
10
16
|
},
|
@@ -73,12 +79,13 @@ module NtqExcelsior
|
|
73
79
|
count
|
74
80
|
end
|
75
81
|
|
76
|
-
def get_styles(row_styles)
|
77
|
-
|
82
|
+
def get_styles(row_styles, cell_styles = [])
|
83
|
+
row_styles ||= []
|
84
|
+
return {} if row_styles.length == 0 && cell_styles.length == 0
|
78
85
|
|
79
86
|
styles_hash = {}
|
80
87
|
stylesheet = styles || {}
|
81
|
-
row_styles.each do |style_key|
|
88
|
+
(row_styles + cell_styles).each do |style_key|
|
82
89
|
styles_hash = styles_hash.merge(stylesheet[style_key] || DEFAULT_STYLES[style_key] || {})
|
83
90
|
end
|
84
91
|
styles_hash
|
@@ -115,14 +122,26 @@ module NtqExcelsior
|
|
115
122
|
end
|
116
123
|
|
117
124
|
def format_value(resolver, record)
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
125
|
+
styles = []
|
126
|
+
type = nil
|
127
|
+
if resolver.is_a?(Proc)
|
128
|
+
value = resolver.call(record)
|
129
|
+
else
|
130
|
+
accessors = resolver
|
131
|
+
accessors = accessors.split(".") if accessors.is_a?(String)
|
132
|
+
value = dig_value(record, accessors)
|
133
|
+
end
|
134
|
+
if value.is_a?(Date)
|
135
|
+
value = value.strftime("%Y-%m-%d")
|
136
|
+
styles << :date_format
|
137
|
+
type = :date
|
138
|
+
end
|
139
|
+
if value.is_a?(Time) | value.is_a?(DateTime)
|
140
|
+
value = value.strftime("%Y-%m-%d %H:%M:%S")
|
141
|
+
styles << :time_format
|
142
|
+
type = :time
|
143
|
+
end
|
144
|
+
{ value: value, styles: styles, type: type }
|
126
145
|
end
|
127
146
|
|
128
147
|
def resolve_record_row(schema, record, index)
|
@@ -130,10 +149,10 @@ module NtqExcelsior
|
|
130
149
|
col_index = 1
|
131
150
|
schema.each do |column|
|
132
151
|
width = column[:width] || 1
|
133
|
-
|
134
|
-
row[:
|
135
|
-
row[:
|
136
|
-
|
152
|
+
formatted_value = format_value(column[:resolve], record)
|
153
|
+
row[:values] << formatted_value[:value]
|
154
|
+
row[:types] << (column[:type] || formatted_value[:type])
|
155
|
+
row[:styles] << get_styles(column[:styles], formatted_value[:styles])
|
137
156
|
if width > 1
|
138
157
|
colspan = width - 1
|
139
158
|
row[:values].push(*Array.new(colspan, nil))
|
@@ -143,7 +162,6 @@ module NtqExcelsior
|
|
143
162
|
|
144
163
|
col_index += 1
|
145
164
|
end
|
146
|
-
|
147
165
|
row
|
148
166
|
end
|
149
167
|
|
@@ -0,0 +1,155 @@
|
|
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 spreadsheet_options(value = nil)
|
11
|
+
@spreadsheet_options ||= value
|
12
|
+
end
|
13
|
+
|
14
|
+
def primary_key(value = nil)
|
15
|
+
@primary_key ||= value
|
16
|
+
end
|
17
|
+
|
18
|
+
def model_klass(value = nil)
|
19
|
+
@model_klass ||= value
|
20
|
+
end
|
21
|
+
|
22
|
+
def schema(value = nil)
|
23
|
+
@schema ||= value
|
24
|
+
end
|
25
|
+
|
26
|
+
def max_error_count(value = nil)
|
27
|
+
@max_error_count ||= value
|
28
|
+
end
|
29
|
+
|
30
|
+
def structure(value = nil)
|
31
|
+
@structure ||= value
|
32
|
+
end
|
33
|
+
|
34
|
+
def sample_file(value = nil)
|
35
|
+
@sample_file ||= value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def spreadsheet
|
40
|
+
return @spreadsheet unless @spreadsheet.nil?
|
41
|
+
|
42
|
+
raise 'File is missing' unless file.present?
|
43
|
+
|
44
|
+
@spreadsheet = Roo::Spreadsheet.open(file, self.class.spreadsheet_options || {})
|
45
|
+
end
|
46
|
+
|
47
|
+
def required_headers
|
48
|
+
return @required_headers if @required_headers
|
49
|
+
|
50
|
+
@required_columns = self.class.schema.select { |field, column_config| !column_config.is_a?(Hash) || !column_config.has_key?(:required) || column_config[:required] }
|
51
|
+
@required_line_keys = @required_columns.map{ |k, v| k }
|
52
|
+
@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}
|
53
|
+
if self.class.primary_key && !@required_line_keys.include?(self.class.primary_key)
|
54
|
+
@required_line_keys = @required_line_keys.unshift(self.class.primary_key)
|
55
|
+
@required_headers = @required_headers.unshift(Regexp.new(self.class.primary_key.to_s, "i"))
|
56
|
+
end
|
57
|
+
@required_headers
|
58
|
+
end
|
59
|
+
|
60
|
+
def spreadsheet_data
|
61
|
+
spreadsheet.sheet(spreadsheet.sheets[0]).parse(header_search: required_headers)
|
62
|
+
end
|
63
|
+
|
64
|
+
def detect_header_scheme(line)
|
65
|
+
return @header_scheme if @header_scheme
|
66
|
+
@header_scheme = {}
|
67
|
+
l = line.dup
|
68
|
+
|
69
|
+
self.class.schema.each do |field, column_config|
|
70
|
+
header = column_config.is_a?(Hash) ? column_config[:header] : column_config
|
71
|
+
|
72
|
+
l.each do |parsed_header, _value|
|
73
|
+
next unless header.is_a?(Regexp) && parsed_header.match?(header) || header.is_a?(String) && parsed_header == header
|
74
|
+
|
75
|
+
l.delete(parsed_header)
|
76
|
+
@header_scheme[parsed_header] = field
|
77
|
+
end
|
78
|
+
end
|
79
|
+
@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]
|
80
|
+
|
81
|
+
@header_scheme
|
82
|
+
end
|
83
|
+
|
84
|
+
def parse_line(line)
|
85
|
+
parsed_line = {}
|
86
|
+
line.each do |header, value|
|
87
|
+
header_scheme = detect_header_scheme(line)
|
88
|
+
if header.to_s == self.class.primary_key.to_s
|
89
|
+
parsed_line[self.class.primary_key] = value
|
90
|
+
next
|
91
|
+
end
|
92
|
+
|
93
|
+
header_scheme.each do |header, field|
|
94
|
+
parsed_line[field.to_sym] = line[header]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
raise Roo::HeaderRowNotFoundError unless (@required_line_keys - parsed_line.keys).size == 0
|
99
|
+
|
100
|
+
parsed_line
|
101
|
+
end
|
102
|
+
|
103
|
+
def lines
|
104
|
+
return @lines if @lines
|
105
|
+
|
106
|
+
@lines = spreadsheet_data.map {|line| parse_line(line) }
|
107
|
+
end
|
108
|
+
|
109
|
+
# id for default query in model
|
110
|
+
# line in case an override is needed to find correct record
|
111
|
+
def find_or_initialize_record(line)
|
112
|
+
raise "Primary key must be set for using the default find_or_initialize" unless self.class.primary_key
|
113
|
+
|
114
|
+
self.class.model_klass.find_or_initialize_by("#{self.class.primary_key}": line[self.class.primary_key.to_sym])
|
115
|
+
end
|
116
|
+
|
117
|
+
def import_line(line, save: true)
|
118
|
+
record = find_or_initialize_record(line)
|
119
|
+
|
120
|
+
yield(record, line) if block_given?
|
121
|
+
|
122
|
+
status = {}
|
123
|
+
return { status: :success } if record.save
|
124
|
+
|
125
|
+
return { status: :error, errors: record.errors.full_messages.join(", ") }
|
126
|
+
end
|
127
|
+
|
128
|
+
def import(save: true, status_tracker: nil)
|
129
|
+
at = 0
|
130
|
+
errors_lines = []
|
131
|
+
success_count = 0
|
132
|
+
lines.each_with_index do |line, index|
|
133
|
+
break if errors_lines.size == self.class.max_error_count
|
134
|
+
|
135
|
+
result = import_line(line.with_indifferent_access, save: true)
|
136
|
+
case result[:status]
|
137
|
+
when :success
|
138
|
+
success_count += 1
|
139
|
+
when :error
|
140
|
+
error_line = line.map { |k, v| v }
|
141
|
+
error_line << result[:errors]
|
142
|
+
errors_lines.push(error_line)
|
143
|
+
end
|
144
|
+
|
145
|
+
if @status_tracker&.is_a?(Proc)
|
146
|
+
at = (((index + 1).to_d / lines.size) * 100.to_d)
|
147
|
+
@status_tracker.call(at)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
{ success_count: success_count, errors: errors_lines }
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
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.0
|
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-24 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
|