From 0db2109b39c009be46567d4861bc25d36b4ce0f9 Mon Sep 17 00:00:00 2001 From: Weston Ganger Date: Thu, 1 Dec 2022 21:04:59 -0800 Subject: [PATCH] Add column_styles option --- CHANGELOG.md | 1 + README.md | 17 +++++---- .../class_methods/xlsx.rb | 30 ++++++++++++--- lib/spreadsheet_architect/utils.rb | 1 + lib/spreadsheet_architect/utils/xlsx.rb | 38 +++++++++++++++++-- .../spreadsheet_architect/exceptions_test.rb | 20 ++++++++++ test/unit/xlsx/general_test.rb | 19 +++++++++- 7 files changed, 108 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b747d39..9352082 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG - **Unreleased** - [View Diff](https://github.com/westonganger/spreadsheet_architect/compare/v5.0.0...master) - [#53](https://github.com/westonganger/spreadsheet_architect/pull/53) - Remove legacy string_width patch for axlsx 3.1 and below - [#54](https://github.com/westonganger/spreadsheet_architect/pull/54) - Fix typo in error message for `:conditional_row_styles` + - [#55](https://github.com/westonganger/spreadsheet_architect/pull/55) - Add option for `:conditional_column_styles` - **5.0.0** - Oct 30, 2022 - [View Diff](https://github.com/westonganger/spreadsheet_architect/compare/v4.2.0...v5.0.0) - [#52](https://github.com/westonganger/spreadsheet_architect/pull/52) - Update to caxlsx v3.3.0+ which now contains the axlsx_styler code, so we drop the dependency on axlsx_styler diff --git a/README.md b/README.md index 6637d97..d8207b7 100644 --- a/README.md +++ b/README.md @@ -206,16 +206,17 @@ end |Option|Default|Notes| |---|---|---| -|**data**
*2D Array*| |Cannot be used with the `:instances` option.

Tabular data for the non-header row cells. | +|**data**
*Nested Array*| |Cannot be used with the `:instances` option.

Tabular data for the non-header row cells.| |**instances**
*Array*| |Cannot be used with the `:data` option.

Array of class/model instances to be used as row data. Cannot be used with :data option| |**spreadsheet_columns**
*Proc/Symbol/String*| Use this option to override or define the spreadsheet columns. Normally, if this option is not specified and are using the instances option/ActiveRecord relation, it uses the classes custom `spreadsheet_columns` method or any custom defaults defined.
If neither of those and is an ActiveRecord model, then it will falls back to the models `self.column_names` | Cannot be used with the `:data` option.

If a Proc value is passed it will be evaluated on the instance object.

If a Symbol or String value is passed then it will search the instance for a method name that matches and call it. | -|**headers**
*Array / 2D Array*| |Data for the header row cells. If using on a class/relation, this defaults to the ones provided via `spreadsheet_columns`. Pass `false` to skip the header row. | +|**headers**
*Array / Nested Array*| |Data for the header row cells. If using on a class/relation, this defaults to the ones provided via `spreadsheet_columns`. Pass `false` to skip the header row. | |**sheet_name**
*String*|`Sheet1`|| |**header_style**
*Hash*|`{background_color: "AAAAAA", color: "FFFFFF", align: :center, font_name: 'Arial', font_size: 10, bold: false, italic: false, underline: false}`|See all available style options [here](./docs/axlsx_style_reference.md)| |**row_style**
*Hash*|`{background_color: nil, color: "000000", align: :left, font_name: 'Arial', font_size: 10, bold: false, italic: false, underline: false, format_code: nil}`|Styles for non-header rows. See all available style options [here](./docs/axlsx_style_reference.md)| |**column_styles**
*Array*||[See the kitchen sink example for usage](./test/unit/xlsx/general_test.rb)| |**range_styles**
*Array*||[See the kitchen sink example for usage](./test/unit/xlsx/general_test.rb)| -|**conditional_row_styles**
*Array*||[See the kitchen sink example for usage](./test/unit/xlsx/general_test.rb). The if/unless proc will called with the following args: `row_index`, `row_data`| +|**conditional_row_styles**
*Array*||[See the kitchen sink example for usage](./test/unit/xlsx/general_test.rb).| +|**conditional_column_styles**
*Array*||[See the kitchen sink example for usage](./test/unit/xlsx/general_test.rb).| |**merges**
*Array*||Merge cells. [See the kitchen sink example for usage](./test/unit/xlsx/general_test.rb). Warning merges cannot overlap eachother, if you attempt to do so Excel will claim your spreadsheet is corrupt and refuse to open your spreadsheet.| |**borders**
*Array*||[See the kitchen sink example for usage](./test/unit/xlsx/general_test.rb)| |**column_types**
*Array*||Valid types for XLSX are :string, :integer, :float, :date, :time, :boolean, :hyperlink, nil = auto determine. You may also pass a Proc which evaluates to any of the valid types, for example `->(cell_val){ cell_val.start_with?('http') ? :hyperlink : :string }`| @@ -233,10 +234,10 @@ Same options as `to_xlsx` |Option|Default|Notes| |---|---|---| -|**data**
*2D Array*| |Cannot be used with the `:instances` option.

Tabular data for the non-header row cells. | +|**data**
*Nested Array*| |Cannot be used with the `:instances` option.

Tabular data for the non-header row cells. | |**instances**
*Array*| |Cannot be used with the `:data` option.

Array of class/model instances to be used as row data. Cannot be used with :data option| |**spreadsheet_columns**
*Proc/Symbol/String*| Use this option to override or define the spreadsheet columns. Normally, if this option is not specified and are using the instances option/ActiveRecord relation, it uses the classes custom `spreadsheet_columns` method or any custom defaults defined.
If neither of those and is an ActiveRecord model, then it will falls back to the models `self.column_names` | Cannot be used with the `:data` option.

If a Proc value is passed it will be evaluated on the instance object.

If a Symbol or String value is passed then it will search the instance for a method name that matches and call it. | -|**headers**
*Array / 2D Array*| |Data for the header row cells. If using on a class/relation, this defaults to the ones provided via `spreadsheet_columns`. Pass `false` to skip the header row. | +|**headers**
*Array / Nested Array*| |Data for the header row cells. If using on a class/relation, this defaults to the ones provided via `spreadsheet_columns`. Pass `false` to skip the header row. | |**sheet_name**
*String*|`Sheet1`|| |**header_style**
*Hash*|`{background_color: "AAAAAA", color: "FFFFFF", align: :center, font_size: 10, bold: true}`|Note: Currently ODS only supports these options| |**row_style**
*Hash*|`{background_color: nil, color: "000000", align: :left, font_size: 10, bold: false}`|Styles for non-header rows. Currently ODS only supports these options| @@ -250,10 +251,10 @@ Same options as `to_ods` |Option|Default|Notes| |---|---|---| -|**data**
*2D Array*| |Cannot be used with the `:instances` option.

Tabular data for the non-header row cells. | +|**data**
*Nested Array*| |Cannot be used with the `:instances` option.

Tabular data for the non-header row cells.| |**instances**
*Array*| |Cannot be used with the `:data` option.

Array of class/model instances to be used as row data. Cannot be used with :data option| |**spreadsheet_columns**
*Proc/Symbol/String*| Use this option to override or define the spreadsheet columns. Normally, if this option is not specified and are using the instances option/ActiveRecord relation, it uses the classes custom `spreadsheet_columns` method or any custom defaults defined.
If neither of those and is an ActiveRecord model, then it will falls back to the models `self.column_names` | Cannot be used with the `:data` option.

If a Proc value is passed it will be evaluated on the instance object.

If a Symbol or String value is passed then it will search the instance for a method name that matches and call it. | -|**headers**
*Array / 2D Array*| |Data for the header row cells. If using on a class/relation, this defaults to the ones provided via `spreadsheet_columns`. Pass `false` to skip the header row. | +|**headers**
*Array / Nested Array*| |Data for the header row cells. If using on a class/relation, this defaults to the ones provided via `spreadsheet_columns`. Pass `false` to skip the header row.| # Change class-wide default method options @@ -278,6 +279,7 @@ class Post < ApplicationRecord column_styles: [], range_styles: [], conditional_row_styles: [], + conditional_column_styles: [], merges: [], borders: [], column_types: [], @@ -299,6 +301,7 @@ SpreadsheetArchitect.default_options = { column_styles: [], range_styles: [], conditional_row_styles: [], + conditional_column_styles: [], merges: [], borders: [], column_types: [], diff --git a/lib/spreadsheet_architect/class_methods/xlsx.rb b/lib/spreadsheet_architect/class_methods/xlsx.rb index 6d03a44..17a61da 100644 --- a/lib/spreadsheet_architect/class_methods/xlsx.rb +++ b/lib/spreadsheet_architect/class_methods/xlsx.rb @@ -45,10 +45,19 @@ def to_axlsx_package(opts={}, package=nil) if options[:conditional_row_styles] conditional_styles_for_row = SpreadsheetArchitect::Utils::XLSX.conditional_styles_for_row(options[:conditional_row_styles], row_index, header_row) - unless conditional_styles_for_row.empty? + sheet.add_style( + "#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES.first}#{row_index+1}:#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES[max_row_length-1]}#{row_index+1}", + SpreadsheetArchitect::Utils::XLSX.convert_styles_to_axlsx(conditional_styles_for_row) + ) + end + + if options[:conditional_column_styles] + conditional_styles_for_columns = SpreadsheetArchitect::Utils::XLSX.conditional_styles_for_columns(options[:conditional_column_styles], row_index, header_row) + + conditional_styles_for_columns.each do |col_name, col_style| sheet.add_style( - "#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES.first}#{row_index+1}:#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES[max_row_length-1]}#{row_index+1}", - SpreadsheetArchitect::Utils::XLSX.convert_styles_to_axlsx(conditional_styles_for_row) + "#{col_name}#{row_index+1}", + SpreadsheetArchitect::Utils::XLSX.convert_styles_to_axlsx(col_style) ) end end @@ -124,10 +133,19 @@ def to_axlsx_package(opts={}, package=nil) 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? + sheet.add_style( + "#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES.first}#{row_index+1}:#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES[max_row_length-1]}#{row_index+1}", + SpreadsheetArchitect::Utils::XLSX.convert_styles_to_axlsx(conditional_styles_for_row) + ) + end + + if options[:conditional_column_styles] + conditional_styles_for_columns = SpreadsheetArchitect::Utils::XLSX.conditional_styles_for_columns(options[:conditional_column_styles], row_index, row_data) + + conditional_styles_for_columns.each do |col_index, col_style| sheet.add_style( - "#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES.first}#{row_index+1}:#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES[max_row_length-1]}#{row_index+1}", - SpreadsheetArchitect::Utils::XLSX.convert_styles_to_axlsx(conditional_styles_for_row) + "#{SpreadsheetArchitect::Utils::XLSX::COL_NAMES[col_index]}#{row_index+1}", + SpreadsheetArchitect::Utils::XLSX.convert_styles_to_axlsx(col_style) ) end end diff --git a/lib/spreadsheet_architect/utils.rb b/lib/spreadsheet_architect/utils.rb index 40f35cb..43392bb 100644 --- a/lib/spreadsheet_architect/utils.rb +++ b/lib/spreadsheet_architect/utils.rb @@ -250,6 +250,7 @@ def self.hash_array_symbolize_keys(array) borders: Array, column_styles: Array, conditional_row_styles: Array, + conditional_column_styles: Array, column_widths: Array, column_types: Array, data: Array, diff --git a/lib/spreadsheet_architect/utils/xlsx.rb b/lib/spreadsheet_architect/utils/xlsx.rb index 2ddfe2d..50501fe 100644 --- a/lib/spreadsheet_architect/utils/xlsx.rb +++ b/lib/spreadsheet_architect/utils/xlsx.rb @@ -2,6 +2,9 @@ module SpreadsheetArchitect module Utils module XLSX + ### Limit of 16384 columns as per Excel limitations + COL_NAMES = Array('A'..'XFD').freeze + def self.get_type(value, type=nil) if type && !type.empty? case type @@ -196,7 +199,7 @@ def self.conditional_styles_for_row(conditional_row_styles, row_index, row_data) conditions_met = !conditions_met end - if conditions_met + if conditions_met && !x[:styles].empty? merged_conditional_styles.merge!(x[:styles]) end end @@ -204,10 +207,37 @@ def self.conditional_styles_for_row(conditional_row_styles, row_index, row_data) return merged_conditional_styles end - private + def self.conditional_styles_for_columns(conditional_column_styles, row_index, row_data) + array = [] - ### Limit of 16384 columns as per Excel limitations - COL_NAMES = Array('A'..'XFD').freeze + conditional_column_styles.each do |x| + if x[:if] && x[:unless] + raise SpreadsheetArchitect::Exceptions::ArgumentError.new('Cannot pass both :if and :unless within the same :conditional_column_styles entry') + elsif !x[:if] && !x[:unless] + raise SpreadsheetArchitect::Exceptions::ArgumentError.new('Must pass either :if or :unless within the each :conditional_column_styles entry') + elsif !x[:styles] + raise SpreadsheetArchitect::Exceptions::ArgumentError.new('Must pass the :styles option within a :conditional_column_styles entry') + elsif !x[:column] + raise SpreadsheetArchitect::Exceptions::ArgumentError.new('Must pass the :column option within a :conditional_column_styles entry') + end + + if x[:column].is_a?(Integer) + x[:column] = SpreadsheetArchitect::Utils::XLSX::COL_NAMES[x[:column]] + end + + conditions_met = (x[:if] || x[:unless]).call(row_data, row_index) + + if x[:unless] + conditions_met = !conditions_met + end + + if conditions_met && !x[:styles].empty? + array << [x[:column], x[:styles]] + end + end + + return array + end end end diff --git a/test/unit/spreadsheet_architect/exceptions_test.rb b/test/unit/spreadsheet_architect/exceptions_test.rb index b79c317..92c9a1f 100644 --- a/test/unit/spreadsheet_architect/exceptions_test.rb +++ b/test/unit/spreadsheet_architect/exceptions_test.rb @@ -20,6 +20,26 @@ class SpreadsheetArchitectExceptionsTest < ActiveSupport::TestCase SpreadsheetArchitect::Utils::XLSX.conditional_styles_for_row(conditional_row_styles, true, true) end + assert_raise error do + conditional_column_styles = [{}] + SpreadsheetArchitect::Utils::XLSX.conditional_styles_for_column(conditional_column_styles, true, true) + end + + assert_raise error do + conditional_column_styles = [{if: true, unless: true, styles: {}}] + SpreadsheetArchitect::Utils::XLSX.conditional_styles_for_column(conditional_column_styles, true, true) + end + + assert_raise error do + conditional_column_styles = [{if: true, styles: false}] + SpreadsheetArchitect::Utils::XLSX.conditional_styles_for_column(conditional_column_styles, true, true) + end + + assert_raise error do + conditional_column_styles = [{if: true, styles: {}, column: nil}] + SpreadsheetArchitect::Utils::XLSX.conditional_styles_for_column(conditional_column_styles, true, true) + end + assert_raise error do SpreadsheetArchitect::Utils.get_options({freeze: {rows: 1}, freeze_headers: true}, SpreadsheetArchitect) end diff --git a/test/unit/xlsx/general_test.rb b/test/unit/xlsx/general_test.rb index 95532bd..92e94cb 100644 --- a/test/unit/xlsx/general_test.rb +++ b/test/unit/xlsx/general_test.rb @@ -8,7 +8,7 @@ class XlsxGeneralTest < ActiveSupport::TestCase ['Latest Posts'], ['Title','Category','Author','Posted on','Posted At','Earnings'] ], - data: 50.times.map{|i| [i, "foobar-#{i}", 5.4*i, true, Date.today, Time.now]}, + data: 50.times.map{|i| [i, "foobar-#{i}", 5.4*i, true, Date.today, Time.now, rand(999,1000)]}, header_style: {background_color: "000000", color: "FFFFFF", align: :center, font_size: 12, bold: true}, row_style: {background_color: nil, color: "000000", align: :left, font_size: 12}, sheet_name: 'Kitchen Sink', @@ -42,6 +42,23 @@ class XlsxGeneralTest < ActiveSupport::TestCase }, ], + conditional_column_styles: [ + { + column: 0, + styles: {bold: true}, + if: Proc.new{|row_data, row_index| + row_index == 0 || row_data[0].to_i == 2 + }, + }, + { + column: "G", + styles: {bold: true}, + unless: Proc.new{|row_data, row_index| + row_data[7].to_i > 1000 + }, + }, + ], + borders: [ {range: "B2:C4"}, {range: "D6:D7", border_styles: {style: :dashDot, color: "333333"}},