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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1f1c651087df6ca7f9dea48884d8976b210294cb95a2577a3048f57ac8488ee
4
- data.tar.gz: ce39afeae3e37049578a191dea797ce8c7cb06be31c3e60fb726cb823a93140f
3
+ metadata.gz: 2f5bdfecae33f17efa14aaac26fcfd4d2ebdc9e0117e5dc924e10c1b36022970
4
+ data.tar.gz: 8771fc83edec065d107912d57e45e8982a804c303ccea0b035bf206f2feaec51
5
5
  SHA512:
6
- metadata.gz: 62c3a791e7fce170b95c2fcd07fa4c9410b57c7aa10570925169cdfa2f38ee603c102a59fafeb49db077ddba9b9644bd6380662105936f12f033c04794d55f33
7
- data.tar.gz: c22d31f130c0d0c3ae5f9a52f346255cb0926ad5c56f93880ba12dc8e622260c12a8e35de0eca1bb2d4a58c50bb4fd3d1d2037c53d377fbf3e618b6ed804db24
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(file.to_stream.read)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NtqExcelsior
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.1"
5
5
  end
data/lib/ntq_excelsior.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "ntq_excelsior/version"
4
4
  require 'ntq_excelsior/exporter'
5
+ require 'ntq_excelsior/importer'
5
6
  module NtqExcelsior
6
7
  class Error < StandardError; end
7
8
  # Your code goes here...
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.3.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-14 00:00:00.000000000 Z
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