spreadsheet_architect 3.3.0 → 4.2.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/CHANGELOG.md +71 -13
- data/README.md +64 -75
- data/lib/spreadsheet_architect/axlsx_string_width_patch.rb +23 -0
- data/lib/spreadsheet_architect/class_methods/csv.rb +2 -0
- data/lib/spreadsheet_architect/class_methods/ods.rb +1 -0
- data/lib/spreadsheet_architect/class_methods/xlsx.rb +56 -8
- data/lib/spreadsheet_architect/utils.rb +84 -25
- data/lib/spreadsheet_architect/utils/xlsx.rb +34 -37
- data/lib/spreadsheet_architect/version.rb +1 -1
- data/test/dummy_app/app/controllers/spreadsheets_controller.rb +5 -2
- data/test/dummy_app/config/application.rb +2 -5
- data/test/dummy_app/config/environments/test.rb +1 -1
- data/test/dummy_app/db/migrate/20170103234524_add_posts.rb +1 -1
- data/test/dummy_app/log/test.log +86427 -0
- data/test/test_helper.rb +15 -7
- data/test/unit/exceptions_test.rb +9 -1
- data/test/unit/kitchen_sink_test.rb +6 -5
- data/test/unit/options_test.rb +9 -0
- data/test/unit/utils_test.rb +43 -2
- data/test/unit/xlsx_freeze_test.rb +44 -0
- data/test/unit/xlsx_utils_test.rb +0 -21
- metadata +30 -15
- data/lib/spreadsheet_architect/monkey_patches/axlsx_column_width.rb +0 -56
- data/test/custom_assertions.rb +0 -21
- data/test/unit/regressions_test.rb +0 -11
@@ -0,0 +1,23 @@
|
|
1
|
+
if Axlsx::VERSION.to_f < 3.1
|
2
|
+
|
3
|
+
Axlsx::Cell.class_eval do
|
4
|
+
private
|
5
|
+
|
6
|
+
def string_width(string, font_size)
|
7
|
+
font_scale = font_size / 10.0
|
8
|
+
(string.to_s.size + 3) * font_scale
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
if defined?(Axlsx::RichTextRun)
|
13
|
+
Axlsx::RichTextRun.class_eval do
|
14
|
+
private
|
15
|
+
|
16
|
+
def string_width(string, font_size)
|
17
|
+
font_scale = font_size / 10.0
|
18
|
+
string.to_s.size * font_scale
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -2,6 +2,7 @@ require 'csv'
|
|
2
2
|
|
3
3
|
module SpreadsheetArchitect
|
4
4
|
module ClassMethods
|
5
|
+
|
5
6
|
def to_csv(opts={})
|
6
7
|
opts = SpreadsheetArchitect::Utils.get_options(opts, self)
|
7
8
|
options = SpreadsheetArchitect::Utils.get_cell_data(opts, self)
|
@@ -18,5 +19,6 @@ module SpreadsheetArchitect
|
|
18
19
|
end
|
19
20
|
end
|
20
21
|
end
|
22
|
+
|
21
23
|
end
|
22
24
|
end
|
@@ -1,12 +1,11 @@
|
|
1
1
|
require 'axlsx'
|
2
2
|
require 'axlsx_styler'
|
3
3
|
|
4
|
-
require 'spreadsheet_architect/
|
4
|
+
require 'spreadsheet_architect/axlsx_string_width_patch'
|
5
5
|
|
6
6
|
module SpreadsheetArchitect
|
7
|
-
XLSX_COLUMN_TYPES = [:string, :integer, :float, :boolean].freeze
|
8
|
-
|
9
7
|
module ClassMethods
|
8
|
+
|
10
9
|
def to_xlsx(opts={})
|
11
10
|
return to_axlsx_package(opts).to_stream.read
|
12
11
|
end
|
@@ -18,7 +17,7 @@ module SpreadsheetArchitect
|
|
18
17
|
if options[:column_types] && !(options[:column_types].compact.collect(&:to_sym) - SpreadsheetArchitect::XLSX_COLUMN_TYPES).empty?
|
19
18
|
raise SpreadsheetArchitect::Exceptions::ArgumentError.new("Invalid column type. Valid XLSX values are #{SpreadsheetArchitect::XLSX_COLUMN_TYPES}")
|
20
19
|
end
|
21
|
-
|
20
|
+
|
22
21
|
header_style = SpreadsheetArchitect::Utils::XLSX.convert_styles_to_axlsx(options[:header_style])
|
23
22
|
row_style = SpreadsheetArchitect::Utils::XLSX.convert_styles_to_axlsx(options[:row_style])
|
24
23
|
|
@@ -48,10 +47,10 @@ module SpreadsheetArchitect
|
|
48
47
|
|
49
48
|
if options[:conditional_row_styles]
|
50
49
|
conditional_styles_for_row = SpreadsheetArchitect::Utils::XLSX.conditional_styles_for_row(options[:conditional_row_styles], row_index, header_row)
|
51
|
-
|
50
|
+
|
52
51
|
unless conditional_styles_for_row.empty?
|
53
52
|
sheet.add_style(
|
54
|
-
"#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES.first}#{row_index+1}:#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES[max_row_length-1]}#{row_index+1}",
|
53
|
+
"#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES.first}#{row_index+1}:#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES[max_row_length-1]}#{row_index+1}",
|
55
54
|
SpreadsheetArchitect::Utils::XLSX.convert_styles_to_axlsx(conditional_styles_for_row)
|
56
55
|
)
|
57
56
|
end
|
@@ -108,11 +107,13 @@ module SpreadsheetArchitect
|
|
108
107
|
sheet.add_row row_data, style: styles, types: types
|
109
108
|
|
110
109
|
if options[:conditional_row_styles]
|
110
|
+
options[:conditional_row_styles] = SpreadsheetArchitect::Utils.hash_array_symbolize_keys(options[:conditional_row_styles])
|
111
|
+
|
111
112
|
conditional_styles_for_row = SpreadsheetArchitect::Utils::XLSX.conditional_styles_for_row(options[:conditional_row_styles], row_index, row_data)
|
112
|
-
|
113
|
+
|
113
114
|
unless conditional_styles_for_row.empty?
|
114
115
|
sheet.add_style(
|
115
|
-
"#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES.first}#{row_index+1}:#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES[max_row_length-1]}#{row_index+1}",
|
116
|
+
"#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES.first}#{row_index+1}:#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES[max_row_length-1]}#{row_index+1}",
|
116
117
|
SpreadsheetArchitect::Utils::XLSX.convert_styles_to_axlsx(conditional_styles_for_row)
|
117
118
|
)
|
118
119
|
end
|
@@ -128,6 +129,8 @@ module SpreadsheetArchitect
|
|
128
129
|
end
|
129
130
|
|
130
131
|
if options[:borders]
|
132
|
+
options[:borders] = SpreadsheetArchitect::Utils.hash_array_symbolize_keys(options[:borders])
|
133
|
+
|
131
134
|
options[:borders].each do |x|
|
132
135
|
if x[:range].is_a?(Hash)
|
133
136
|
x[:range] = SpreadsheetArchitect::Utils::XLSX.range_hash_to_str(x[:range], max_row_length, num_rows)
|
@@ -140,6 +143,8 @@ module SpreadsheetArchitect
|
|
140
143
|
end
|
141
144
|
|
142
145
|
if options[:column_styles]
|
146
|
+
options[:column_styles] = SpreadsheetArchitect::Utils.hash_array_symbolize_keys(options[:column_styles])
|
147
|
+
|
143
148
|
options[:column_styles].each do |x|
|
144
149
|
start_row = (options[:headers] ? options[:headers].count : 0) + 1
|
145
150
|
|
@@ -171,6 +176,8 @@ module SpreadsheetArchitect
|
|
171
176
|
end
|
172
177
|
|
173
178
|
if options[:range_styles]
|
179
|
+
options[:range_styles] = SpreadsheetArchitect::Utils.hash_array_symbolize_keys(options[:range_styles])
|
180
|
+
|
174
181
|
options[:range_styles].each do |x|
|
175
182
|
styles = SpreadsheetArchitect::Utils::XLSX.convert_styles_to_axlsx(x[:styles])
|
176
183
|
|
@@ -185,6 +192,8 @@ module SpreadsheetArchitect
|
|
185
192
|
end
|
186
193
|
|
187
194
|
if options[:merges]
|
195
|
+
options[:merges] = SpreadsheetArchitect::Utils.hash_array_symbolize_keys(options[:merges])
|
196
|
+
|
188
197
|
options[:merges].each do |x|
|
189
198
|
if x[:range].is_a?(Hash)
|
190
199
|
x[:range] = SpreadsheetArchitect::Utils::XLSX.range_hash_to_str(x[:range], max_row_length, num_rows)
|
@@ -195,9 +204,48 @@ module SpreadsheetArchitect
|
|
195
204
|
sheet.merge_cells x[:range]
|
196
205
|
end
|
197
206
|
end
|
207
|
+
|
208
|
+
if options[:freeze_headers]
|
209
|
+
sheet.sheet_view.pane do |pane|
|
210
|
+
pane.state = :frozen
|
211
|
+
pane.y_split = options[:headers].count
|
212
|
+
end
|
213
|
+
|
214
|
+
elsif options[:freeze]
|
215
|
+
options[:freeze] = SpreadsheetArchitect::Utils.symbolize_keys(options[:freeze])
|
216
|
+
|
217
|
+
sheet.sheet_view.pane do |pane|
|
218
|
+
pane.state = :frozen
|
219
|
+
|
220
|
+
### Currently not working
|
221
|
+
#if options[:freeze][:active_pane]
|
222
|
+
# Axlsx.validate_pane_type(options[:freeze][:active_pane])
|
223
|
+
# pane.active_pane = options[:freeze][:active_pane]
|
224
|
+
#else
|
225
|
+
# pane.active_pane = :bottom_right
|
226
|
+
#end
|
227
|
+
|
228
|
+
if !options[:freeze][:rows]
|
229
|
+
raise SpreadsheetArchitect::Exceptions::ArgumentError.new("The :rows key must be specified in the :freeze option hash")
|
230
|
+
elsif options[:freeze][:rows].is_a?(Range)
|
231
|
+
pane.y_split = options[:freeze][:rows].count
|
232
|
+
else
|
233
|
+
pane.y_split = 1
|
234
|
+
end
|
235
|
+
|
236
|
+
if options[:freeze][:columns] && options[:freeze][:columns] != :all
|
237
|
+
if options[:freeze][:columns].is_a?(Range)
|
238
|
+
pane.x_split = options[:freeze][:columns].count
|
239
|
+
else
|
240
|
+
pane.x_split = 1
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
198
245
|
end
|
199
246
|
|
200
247
|
return package
|
201
248
|
end
|
249
|
+
|
202
250
|
end
|
203
251
|
end
|
@@ -7,9 +7,12 @@ module SpreadsheetArchitect
|
|
7
7
|
data = options[:data]
|
8
8
|
end
|
9
9
|
|
10
|
-
if
|
10
|
+
if options[:headers] == true
|
11
11
|
headers = []
|
12
|
-
|
12
|
+
|
13
|
+
if !options[:data]
|
14
|
+
needs_headers = true
|
15
|
+
end
|
13
16
|
elsif options[:headers].is_a?(Array)
|
14
17
|
headers = options[:headers]
|
15
18
|
else
|
@@ -109,20 +112,22 @@ module SpreadsheetArchitect
|
|
109
112
|
def self.get_options(options, klass)
|
110
113
|
verify_option_types(options)
|
111
114
|
|
112
|
-
if
|
113
|
-
if klass::SPREADSHEET_OPTIONS
|
114
|
-
|
115
|
-
klass::SPREADSHEET_OPTIONS
|
116
|
-
|
115
|
+
if !options[:skip_defaults]
|
116
|
+
if defined?(klass::SPREADSHEET_OPTIONS)
|
117
|
+
if klass::SPREADSHEET_OPTIONS.is_a?(Hash)
|
118
|
+
defaults = SpreadsheetArchitect.default_options.merge(klass::SPREADSHEET_OPTIONS)
|
119
|
+
else
|
120
|
+
raise SpreadsheetArchitect::Exceptions::OptionTypeError.new("#{klass}::SPREADSHEET_OPTIONS constant")
|
121
|
+
end
|
117
122
|
else
|
118
|
-
|
123
|
+
defaults = SpreadsheetArchitect.default_options
|
119
124
|
end
|
120
|
-
|
121
|
-
options =
|
125
|
+
|
126
|
+
options = defaults.merge(options)
|
122
127
|
end
|
123
128
|
|
124
129
|
if !options[:headers]
|
125
|
-
options
|
130
|
+
options.delete(:header_style)
|
126
131
|
end
|
127
132
|
|
128
133
|
if !options[:sheet_name]
|
@@ -137,6 +142,14 @@ module SpreadsheetArchitect
|
|
137
142
|
end
|
138
143
|
end
|
139
144
|
|
145
|
+
if options[:freeze]
|
146
|
+
if options[:freeze_headers]
|
147
|
+
raise SpreadsheetArchitect::Exceptions::ArgumentError.new('Cannot use both :freeze and :freeze_headers options at the same time')
|
148
|
+
elsif options[:freeze].is_a?(Hash) && !options[:freeze][:rows]
|
149
|
+
raise SpreadsheetArchitect::Exceptions::ArgumentError.new('Must provide a :rows key when passing a hash to the :freeze option')
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
140
153
|
return options
|
141
154
|
end
|
142
155
|
|
@@ -208,23 +221,22 @@ module SpreadsheetArchitect
|
|
208
221
|
end
|
209
222
|
|
210
223
|
def self.verify_option_types(options)
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
check_option_type(options, :column_widths, Array)
|
223
|
-
check_option_type(options, :column_types, Array)
|
224
|
+
options = self.symbolize_keys(options, shallow: true)
|
225
|
+
|
226
|
+
bad_keys = options.keys - ALLOWED_OPTIONS.keys
|
227
|
+
|
228
|
+
if bad_keys.any?
|
229
|
+
raise SpreadsheetArchitect::Exceptions::ArgumentError.new("Invalid options provided: #{bad_keys}")
|
230
|
+
end
|
231
|
+
|
232
|
+
ALLOWED_OPTIONS.each do |key, allowed_types|
|
233
|
+
check_option_type(options, key, allowed_types)
|
234
|
+
end
|
224
235
|
end
|
225
236
|
|
226
|
-
def self.stringify_keys(hash
|
237
|
+
def self.stringify_keys(hash)
|
227
238
|
new_hash = {}
|
239
|
+
|
228
240
|
hash.each do |k,v|
|
229
241
|
if v.is_a?(Hash)
|
230
242
|
new_hash[k.to_s] = self.stringify_keys(v)
|
@@ -232,7 +244,54 @@ module SpreadsheetArchitect
|
|
232
244
|
new_hash[k.to_s] = v
|
233
245
|
end
|
234
246
|
end
|
247
|
+
|
235
248
|
return new_hash
|
236
249
|
end
|
250
|
+
|
251
|
+
def self.symbolize_keys(hash, shallow: false)
|
252
|
+
new_hash = {}
|
253
|
+
|
254
|
+
hash.each do |k,v|
|
255
|
+
if v.is_a?(Hash)
|
256
|
+
new_hash[k.to_sym] = shallow ? v : self.symbolize_keys(v)
|
257
|
+
else
|
258
|
+
new_hash[k.to_sym] = v
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
return new_hash
|
263
|
+
end
|
264
|
+
|
265
|
+
def self.hash_array_symbolize_keys(array)
|
266
|
+
new_array = []
|
267
|
+
|
268
|
+
array.each_with_index do |x,i|
|
269
|
+
new_array[i] = x.is_a?(Hash) ? self.symbolize_keys(x) : x
|
270
|
+
end
|
271
|
+
|
272
|
+
return new_array
|
273
|
+
end
|
274
|
+
|
275
|
+
ALLOWED_OPTIONS = {
|
276
|
+
borders: Array,
|
277
|
+
column_styles: Array,
|
278
|
+
conditional_row_styles: Array,
|
279
|
+
column_widths: Array,
|
280
|
+
column_types: Array,
|
281
|
+
data: Array,
|
282
|
+
freeze_headers: [TrueClass, FalseClass],
|
283
|
+
freeze: Hash,
|
284
|
+
headers: [TrueClass, FalseClass, Array],
|
285
|
+
header_style: Hash,
|
286
|
+
instances: Array,
|
287
|
+
merges: Array,
|
288
|
+
range_styles: Array,
|
289
|
+
skip_defaults: [TrueClass, FalseClass],
|
290
|
+
row_style: Hash,
|
291
|
+
sheet_name: String,
|
292
|
+
spreadsheet_columns: [Proc, Symbol, String],
|
293
|
+
}.freeze
|
294
|
+
|
295
|
+
|
237
296
|
end
|
238
297
|
end
|
@@ -25,8 +25,29 @@ module SpreadsheetArchitect
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def self.convert_styles_to_axlsx(styles={})
|
28
|
-
|
29
|
-
|
28
|
+
if [nil, false, true].include?(styles)
|
29
|
+
return {}
|
30
|
+
end
|
31
|
+
|
32
|
+
styles = SpreadsheetArchitect::Utils.symbolize_keys(styles)
|
33
|
+
|
34
|
+
### BOOLEAN VALUES
|
35
|
+
if styles[:b].nil? && styles.has_key?(:bold)
|
36
|
+
styles[:b] = !!styles.delete(:bold)
|
37
|
+
end
|
38
|
+
|
39
|
+
if styles[:i].nil? && styles.has_key?(:italic)
|
40
|
+
styles[:i] = !!styles.delete(:italic)
|
41
|
+
end
|
42
|
+
|
43
|
+
if styles[:u].nil? && styles.has_key?(:underline)
|
44
|
+
styles[:u] = !!styles.delete(:underline)
|
45
|
+
end
|
46
|
+
|
47
|
+
### OTHER VALUES
|
48
|
+
if styles[:sz].nil? && !styles[:font_size].nil?
|
49
|
+
styles[:sz] = styles.delete(:font_size)
|
50
|
+
end
|
30
51
|
|
31
52
|
if styles[:fg_color].nil? && styles[:color] && styles[:color].respond_to?(:sub) && !styles[:color].empty?
|
32
53
|
styles[:fg_color] = styles.delete(:color).sub('#','')
|
@@ -36,7 +57,7 @@ module SpreadsheetArchitect
|
|
36
57
|
styles[:bg_color] = styles.delete(:background_color).sub('#','')
|
37
58
|
end
|
38
59
|
|
39
|
-
if styles[:alignment].nil? &&
|
60
|
+
if styles[:alignment].nil? && [:align, :valign, :wrap_text].any?{|k| styles.has_key?(k) }
|
40
61
|
if styles[:align].is_a?(Hash)
|
41
62
|
styles[:alignment] = {
|
42
63
|
horizontal: styles[:align][:horizontal],
|
@@ -44,34 +65,22 @@ module SpreadsheetArchitect
|
|
44
65
|
wrap_text: styles[:align][:wrap_text]
|
45
66
|
}
|
46
67
|
else
|
47
|
-
styles[:alignment] = {horizontal:
|
68
|
+
styles[:alignment] = {horizontal: styles.delete(:align), vertical: styles.delete(:valign), wrap_text: styles.delete(:wrap_text) }.compact
|
48
69
|
end
|
49
|
-
|
50
|
-
styles.delete(:align)
|
51
|
-
end
|
52
|
-
|
53
|
-
if styles[:b].nil?
|
54
|
-
styles[:b] = styles.delete(:bold) || nil
|
55
|
-
end
|
56
|
-
|
57
|
-
if styles[:sz].nil?
|
58
|
-
styles[:sz] = styles.delete(:font_size) || nil
|
59
|
-
end
|
60
|
-
|
61
|
-
if styles[:i].nil?
|
62
|
-
styles[:i] = styles.delete(:italic) || nil
|
63
|
-
end
|
64
|
-
|
65
|
-
if styles[:u].nil?
|
66
|
-
styles[:u] = styles.delete(:underline) || nil
|
67
70
|
end
|
68
71
|
|
72
|
+
### COMMENT SEEMS WRONG, TO BE RECONFIRMED
|
69
73
|
### If `:u` is false instead of nil, it may be incorrectly rendered as true in Excel
|
70
|
-
if styles[:u] == false
|
71
|
-
|
74
|
+
#if styles[:u] == false
|
75
|
+
# styles[:u] = nil
|
76
|
+
#end
|
77
|
+
|
78
|
+
### ENSURE CLEANUP OF ALL ALIAS KEYS
|
79
|
+
[:bold, :font_size, :italic, :underline, :align, :valign, :wrap_text, :color, :background_color].each do |k|
|
80
|
+
styles.delete(k)
|
72
81
|
end
|
73
82
|
|
74
|
-
styles
|
83
|
+
return styles
|
75
84
|
end
|
76
85
|
|
77
86
|
def self.range_hash_to_str(hash, num_columns, num_rows)
|
@@ -176,18 +185,6 @@ module SpreadsheetArchitect
|
|
176
185
|
|
177
186
|
private
|
178
187
|
|
179
|
-
def self.symbolize_keys(hash={})
|
180
|
-
new_hash = {}
|
181
|
-
hash.each do |k, v|
|
182
|
-
if v.is_a?(Hash)
|
183
|
-
new_hash[k.to_sym] = self.symbolize_keys(v)
|
184
|
-
else
|
185
|
-
new_hash[k.to_sym] = v
|
186
|
-
end
|
187
|
-
end
|
188
|
-
new_hash
|
189
|
-
end
|
190
|
-
|
191
188
|
### Limit of 16384 columns as per Excel limitations
|
192
189
|
COL_NAMES = Array('A'..'XFD').freeze
|
193
190
|
|