Skip to content

Commit

Permalink
Complete xlsx freeze implementation, ensure all option hashes are con…
Browse files Browse the repository at this point in the history
…verted to symbol keys
  • Loading branch information
westonganger committed Feb 6, 2020
1 parent b31a787 commit a8febce
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 101 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Expand Up @@ -4,7 +4,9 @@ CHANGELOG
- **Unreleased**
- Switch to the `caxlsx` gem (Community Axlsx) from the legacy unmaintained `axlsx` gem. Axlsx has had a long history of being poorly maintained so this community gem improves the situation.
- Require Ruby 2.3+
- Add options `:freeze` and `:freeze_headers` for XLSX spreadsheets
- Ensure all options using Hash are automatically converted to symbol only hashes
- Add XLSX option `:freeze` to freeze custom sections of your spreadsheet
- Add XLSX option `:freeze_headers` to freeze the headers of your spreadsheet
- **3.3.1**
- [Issue #30](https://github.com/westonganger/spreadsheet_architect/issues/30) - Fix duplicate constant warning for XLSX_COLUMN_TYPES
- **3.3.0**
Expand Down
55 changes: 34 additions & 21 deletions lib/spreadsheet_architect/class_methods/xlsx.rb
Expand Up @@ -107,6 +107,8 @@ def to_axlsx_package(opts={}, package=nil)
sheet.add_row row_data, style: styles, types: types

if options[:conditional_row_styles]
options[:conditional_row_styles] = SpreadsheetArchitect::Utils.hash_array_symbolize_keys(options[:conditional_row_styles])

conditional_styles_for_row = SpreadsheetArchitect::Utils::XLSX.conditional_styles_for_row(options[:conditional_row_styles], row_index, row_data)

unless conditional_styles_for_row.empty?
Expand All @@ -127,6 +129,8 @@ def to_axlsx_package(opts={}, package=nil)
end

if options[:borders]
options[:borders] = SpreadsheetArchitect::Utils.hash_array_symbolize_keys(options[:borders])

options[:borders].each do |x|
if x[:range].is_a?(Hash)
x[:range] = SpreadsheetArchitect::Utils::XLSX.range_hash_to_str(x[:range], max_row_length, num_rows)
Expand All @@ -139,6 +143,8 @@ def to_axlsx_package(opts={}, package=nil)
end

if options[:column_styles]
options[:column_styles] = SpreadsheetArchitect::Utils.hash_array_symbolize_keys(options[:column_styles])

options[:column_styles].each do |x|
start_row = (options[:headers] ? options[:headers].count : 0) + 1

Expand Down Expand Up @@ -170,6 +176,8 @@ def to_axlsx_package(opts={}, package=nil)
end

if options[:range_styles]
options[:range_styles] = SpreadsheetArchitect::Utils.hash_array_symbolize_keys(options[:range_styles])

options[:range_styles].each do |x|
styles = SpreadsheetArchitect::Utils::XLSX.convert_styles_to_axlsx(x[:styles])

Expand All @@ -184,6 +192,8 @@ def to_axlsx_package(opts={}, package=nil)
end

if options[:merges]
options[:merges] = SpreadsheetArchitect::Utils.hash_array_symbolize_keys(options[:merges])

options[:merges].each do |x|
if x[:range].is_a?(Hash)
x[:range] = SpreadsheetArchitect::Utils::XLSX.range_hash_to_str(x[:range], max_row_length, num_rows)
Expand All @@ -195,38 +205,41 @@ def to_axlsx_package(opts={}, package=nil)
end
end


if options[:freeze_headers]
sheet.sheet_view.pane do |pane|
pane.top_left_cell = "A1"
pane.state = :frozen_split
pane.active_pane = :bottom_right
pane.state = :frozen
pane.y_split = options[:headers].count
end

elsif options[:freeze]
options[:freeze] = SpreadsheetArchitect::Utils.symbolize_keys(options[:freeze])

sheet.sheet_view.pane do |pane|
pane.state = :frozen_split
pane.active_pane = :bottom_right

if !options[:freeze][:columns] || options[:freeze][:columns] == :all
freeze_cell = "A"
elsif options[:freeze][:columns].is_a?(Range)
freeze_cell = "#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES[options[:freeze][:columns].first]}"
pane.y_split = options[:freeze][:columns].count
pane.state = :frozen

### Currently not working
#if options[:freeze][:active_pane]
# Axlsx.validate_pane_type(options[:freeze][:active_pane])
# pane.active_pane = options[:freeze][:active_pane]
#else
# pane.active_pane = :bottom_right
#end

if !options[:freeze][:rows]
raise SpreadsheetArchitect::Exceptions::ArgumentError.new("The :rows key must be specified in the :freeze option hash")
elsif options[:freeze][:rows].is_a?(Range)
pane.y_split = options[:freeze][:rows].count
else
freeze_cell = "#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES[options[:freeze][:columns]]}"
pane.y_split = 1
end

if options[:freeze][:rows].is_a?(Range)
freeze_cell += "#{options[:freeze][:rows].first}"
pane.x_split = options[:freeze][:rows].count
else
freeze_cell += "#{options[:freeze][:rows]}"
pane.x_split = 1
if options[:freeze][:columns] && options[:freeze][:columns] != :all
if options[:freeze][:columns].is_a?(Range)
pane.x_split = options[:freeze][:columns].count
else
pane.x_split = 1
end
end

pane.top_left_cell = freeze_cell
end
end
end
Expand Down
31 changes: 30 additions & 1 deletion lib/spreadsheet_architect/utils.rb
Expand Up @@ -216,6 +216,8 @@ def self.check_option_type(options, option_name, type)
end

def self.verify_option_types(options)
options = self.symbolize_keys(options, shallow: true)

check_option_type(options, :spreadsheet_columns, [Proc, Symbol, String])
check_option_type(options, :data, Array)
check_option_type(options, :instances, Array)
Expand All @@ -233,16 +235,43 @@ def self.verify_option_types(options)
check_option_type(options, :freeze, Hash)
end

def self.stringify_keys(hash={})
def self.stringify_keys(hash)
new_hash = {}

hash.each do |k,v|
if v.is_a?(Hash)
new_hash[k.to_s] = self.stringify_keys(v)
else
new_hash[k.to_s] = v
end
end

return new_hash
end

def self.symbolize_keys(hash, shallow: false)
new_hash = {}

hash.each do |k,v|
if v.is_a?(Hash)
new_hash[k.to_sym] = shallow ? v : self.symbolize_keys(v)
else
new_hash[k.to_sym] = v
end
end

return new_hash
end

def self.hash_array_symbolize_keys(array)
new_array = []

array.each_with_index do |x,i|
new_array[i] = x.is_a?(Hash) ? self.symbolize_keys(x) : x
end

return new_array
end

end
end
15 changes: 2 additions & 13 deletions lib/spreadsheet_architect/utils/xlsx.rb
Expand Up @@ -26,7 +26,8 @@ def self.get_type(value, type=nil)

def self.convert_styles_to_axlsx(styles={})
styles = {} unless styles.is_a?(Hash)
styles = self.symbolize_keys(styles)

styles = SpreadsheetArchitect::Utils.symbolize_keys(styles)

if styles[:fg_color].nil? && styles[:color] && styles[:color].respond_to?(:sub) && !styles[:color].empty?
styles[:fg_color] = styles.delete(:color).sub('#','')
Expand Down Expand Up @@ -176,18 +177,6 @@ def self.conditional_styles_for_row(conditional_row_styles, row_index, row_data)

private

def self.symbolize_keys(hash={})
new_hash = {}
hash.each do |k, v|
if v.is_a?(Hash)
new_hash[k.to_sym] = self.symbolize_keys(v)
else
new_hash[k.to_sym] = v
end
end
new_hash
end

### Limit of 16384 columns as per Excel limitations
COL_NAMES = Array('A'..'XFD').freeze

Expand Down
5 changes: 1 addition & 4 deletions test/dummy_app/config/application.rb
Expand Up @@ -3,10 +3,7 @@
require 'rails/all'

require 'axlsx'
silence_warnings do
### required until monkey patch in axlsx_styler is updated to Ruby 2.0 prepend syntax
require 'axlsx_styler'
end
require 'axlsx_styler'

Bundler.require

Expand Down
5 changes: 0 additions & 5 deletions test/unit/exceptions_test.rb
Expand Up @@ -23,11 +23,6 @@ class ExceptionsTest < ActiveSupport::TestCase
assert_raise error do
SpreadsheetArchitect::Utils.get_options({freeze: {rows: 1}, freeze_headers: true}, SpreadsheetArchitect)
end

assert_nothing_raised do
SpreadsheetArchitect.default_options = {freeze_headers: true}
SpreadsheetArchitect::Utils.get_options({freeze: {rows: 1}}, SpreadsheetArchitect)
end
end

test "NoDataError" do
Expand Down
8 changes: 4 additions & 4 deletions test/unit/kitchen_sink_test.rb
Expand Up @@ -20,7 +20,7 @@ def teardown
end

def test_xlsx
@options.merge!({
opts = @options.merge({
column_styles: [
{columns: 0, styles: {bold: true}},
{columns: (1..3), styles: {color: "444444"}},
Expand Down Expand Up @@ -74,15 +74,15 @@ def test_xlsx
})

# Using Array Data
file_data = SpreadsheetArchitect.to_xlsx(@options)
file_data = SpreadsheetArchitect.to_xlsx(opts)

File.open(VERSIONED_BASE_PATH.join("kitchen_sink.xlsx"),'w+b') do |f|
f.write file_data
end
end

def test_ods
@options.merge!({
opts = @options.merge({
headers: [
['Latest Posts'],
['Title','Category','Author','Boolean','Posted on','Posted At']
Expand All @@ -100,7 +100,7 @@ def test_ods
})

# Using Array Data
file_data = SpreadsheetArchitect.to_ods(@options)
file_data = SpreadsheetArchitect.to_ods(opts)

File.open(VERSIONED_BASE_PATH.join("kitchen_sink.ods"),'w+b') do |f|
f.write file_data
Expand Down
33 changes: 32 additions & 1 deletion test/unit/utils_test.rb
Expand Up @@ -153,7 +153,7 @@ def teardown
end

test "stringify_keys" do
hash = klass.stringify_keys
hash = klass.stringify_keys({})
assert_empty hash

hash = klass.stringify_keys({foo: :bar})
Expand All @@ -176,4 +176,35 @@ def teardown
assert_nil hash[:foo]
assert_equal hash['foo']['foo']['foo']['foo'], :bar
end

test "symbolize_keys" do
hash = klass.symbolize_keys({})
assert_empty hash

hash = klass.symbolize_keys({'foo' => :bar})
assert_nil hash['foo']
assert_equal hash[:foo], :bar

hash = klass.symbolize_keys({'foo' => :bar, bar: :foo})
assert_nil hash['foo']
assert_equal hash[:foo], :bar

hash = klass.symbolize_keys({'foo' => {'foo' => :bar}})
assert_nil hash['foo']
assert_equal hash[:foo][:foo], :bar

hash = klass.symbolize_keys({'foo' => {'foo' => {'foo' => :bar}}})
assert_nil hash['foo']
assert_equal hash[:foo][:foo][:foo], :bar
end

test "hash_array_symbolize_keys" do
array = klass.hash_array_symbolize_keys([])
assert_empty array

array = klass.hash_array_symbolize_keys([{'foo' => :bar}])
assert_nil array[0]['foo']
assert_equal array[0][:foo], :bar
end

end
34 changes: 4 additions & 30 deletions test/unit/xlsx_freeze_test.rb
Expand Up @@ -16,55 +16,29 @@ def teardown
end

def test_1
@options.merge!({
opts = @options.merge({
freeze: {rows: 1, columns: 1},
})

# Using Array Data
file_data = SpreadsheetArchitect.to_xlsx(@options)
file_data = SpreadsheetArchitect.to_xlsx(opts)

File.open(VERSIONED_BASE_PATH.join("freeze_test_1.xlsx"),'w+b') do |f|
f.write file_data
end
end

def test_2
@options.merge!({
opts = @options.merge({
freeze: {rows: (2..4), columns: (2..4)},
})

# Using Array Data
file_data = SpreadsheetArchitect.to_xlsx(@options)
file_data = SpreadsheetArchitect.to_xlsx(opts)

File.open(VERSIONED_BASE_PATH.join("freeze_test_2.xlsx"),'w+b') do |f|
f.write file_data
end
end

def test_ods
@options.merge!({
headers: [
['Latest Posts'],
['Title','Category','Author','Boolean','Posted on','Posted At']
],
data: 50.times.map{|i| [i, "foobar-#{i}", (5.4*i), true, Date.today, Time.now]},
column_types: [
:string,
:float,
:float,
:boolean,
:date,
:time,
nil
],
})

# Using Array Data
file_data = SpreadsheetArchitect.to_ods(@options)

File.open(VERSIONED_BASE_PATH.join("kitchen_sink.ods"),'w+b') do |f|
f.write file_data
end
end

end
21 changes: 0 additions & 21 deletions test/unit/xlsx_utils_test.rb
Expand Up @@ -113,27 +113,6 @@ def teardown
#end
end

test "symbolize_keys" do
hash = klass.symbolize_keys
assert_empty hash

hash = klass.symbolize_keys({'foo' => :bar})
assert_nil hash['foo']
assert_equal hash[:foo], :bar

hash = klass.symbolize_keys({'foo' => :bar, bar: :foo})
assert_nil hash['foo']
assert_equal hash[:foo], :bar

hash = klass.symbolize_keys({'foo' => {'foo' => :bar}})
assert_nil hash['foo']
assert_equal hash[:foo][:foo], :bar

hash = klass.symbolize_keys({'foo' => {'foo' => {'foo' => :bar}}})
assert_nil hash['foo']
assert_equal hash[:foo][:foo][:foo], :bar
end

test "constants" do
assert_equal klass::COL_NAMES.count, 16384
assert_equal klass::COL_NAMES.first, 'A'
Expand Down

0 comments on commit a8febce

Please sign in to comment.