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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 84b27eb6270d0a9527ca77e3c3b9d93328ef2151b14703c796bc07565a16767a
4
- data.tar.gz: ef00357a9c9a2e1c1bc54d6b3ecbe2cc935ce8ba93452acba4d6ecd6a9ed1d94
3
+ metadata.gz: 4443b707f72d72748af1e8d5c728aef25ea2f7175704f653bebf2b1897ea1784
4
+ data.tar.gz: 7fdc4700b25c1d5773638186fb5e22f2afd9fa956cf755a5f4c6eccf4fb4f5d3
5
5
  SHA512:
6
- metadata.gz: 830dd162264d62ac828b52d6dd428f251936b2659fb92e5b34ed3e6809bc84e5b2165c804b637685c1e2a82afcbda86685c9afc663149ce000c103ad0954157f
7
- data.tar.gz: 0baa0a8bc9807bc405d55598d498d731239a1a39823c044784a9ea7c352258ce3b9216d3ded2a329c521a908b0a1ac9f0c7d72bbaa56c09f1ae788e00aca75f5
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(file.to_stream.read)
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
- return {} unless row_styles && row_styles.length > 0
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
- return resolver.call(record) if resolver.is_a?(Proc)
119
-
120
- accessors = resolver
121
- accessors = accessors.split(".") if accessors.is_a?(String)
122
- value = dig_value(record, accessors)
123
- value = value.strftime("%Y-%m-%d") if value.is_a?(Date)
124
- value = value.strftime("%Y-%m-%d %H:%M:%S") if value.is_a?(Time) | value.is_a?(DateTime)
125
- value
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
- row[:values] << format_value(column[:resolve], record)
134
- row[:types] << column[:type] || :string
135
- row[:styles] << get_styles(column[:styles])
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NtqExcelsior
4
- VERSION = "0.2.0"
4
+ VERSION = "0.4.0"
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.2.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-03 00:00:00.000000000 Z
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