ntq_excelsior 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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