Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add conditional_column_styles option #58

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,16 +206,17 @@ end

|Option|Default|Notes|
|---|---|---|
|**data**<br>*2D Array*| |Cannot be used with the `:instances` option.<br><br>Tabular data for the non-header row cells. |
|**data**<br>*Nested Array*| |Cannot be used with the `:instances` option.<br><br>Tabular data for the non-header row cells.|
|**instances**<br>*Array*| |Cannot be used with the `:data` option.<br><br>Array of class/model instances to be used as row data. Cannot be used with :data option|
|**spreadsheet_columns**<br>*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.<br>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.<br><br>If a Proc value is passed it will be evaluated on the instance object.<br><br>If a Symbol or String value is passed then it will search the instance for a method name that matches and call it. |
|**headers**<br>*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**<br>*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**<br>*String*|`Sheet1`||
|**header_style**<br>*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**<br>*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**<br>*Array*||[See the kitchen sink example for usage](./test/unit/xlsx/general_test.rb)|
|**range_styles**<br>*Array*||[See the kitchen sink example for usage](./test/unit/xlsx/general_test.rb)|
|**conditional_row_styles**<br>*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**<br>*Array*||[See the kitchen sink example for usage](./test/unit/xlsx/general_test.rb).|
|**conditional_column_styles**<br>*Array*||[See the kitchen sink example for usage](./test/unit/xlsx/general_test.rb).|
|**merges**<br>*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**<br>*Array*||[See the kitchen sink example for usage](./test/unit/xlsx/general_test.rb)|
|**column_types**<br>*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 }`|
Expand All @@ -233,10 +234,10 @@ Same options as `to_xlsx`

|Option|Default|Notes|
|---|---|---|
|**data**<br>*2D Array*| |Cannot be used with the `:instances` option.<br><br>Tabular data for the non-header row cells. |
|**data**<br>*Nested Array*| |Cannot be used with the `:instances` option.<br><br>Tabular data for the non-header row cells. |
|**instances**<br>*Array*| |Cannot be used with the `:data` option.<br><br>Array of class/model instances to be used as row data. Cannot be used with :data option|
|**spreadsheet_columns**<br>*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.<br>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.<br><br>If a Proc value is passed it will be evaluated on the instance object.<br><br>If a Symbol or String value is passed then it will search the instance for a method name that matches and call it. |
|**headers**<br>*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**<br>*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**<br>*String*|`Sheet1`||
|**header_style**<br>*Hash*|`{background_color: "AAAAAA", color: "FFFFFF", align: :center, font_size: 10, bold: true}`|Note: Currently ODS only supports these options|
|**row_style**<br>*Hash*|`{background_color: nil, color: "000000", align: :left, font_size: 10, bold: false}`|Styles for non-header rows. Currently ODS only supports these options|
Expand All @@ -250,10 +251,10 @@ Same options as `to_ods`

|Option|Default|Notes|
|---|---|---|
|**data**<br>*2D Array*| |Cannot be used with the `:instances` option.<br><br>Tabular data for the non-header row cells. |
|**data**<br>*Nested Array*| |Cannot be used with the `:instances` option.<br><br>Tabular data for the non-header row cells.|
|**instances**<br>*Array*| |Cannot be used with the `:data` option.<br><br>Array of class/model instances to be used as row data. Cannot be used with :data option|
|**spreadsheet_columns**<br>*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.<br>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.<br><br>If a Proc value is passed it will be evaluated on the instance object.<br><br>If a Symbol or String value is passed then it will search the instance for a method name that matches and call it. |
|**headers**<br>*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**<br>*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
Expand All @@ -278,6 +279,7 @@ class Post < ApplicationRecord
column_styles: [],
range_styles: [],
conditional_row_styles: [],
conditional_column_styles: [],
merges: [],
borders: [],
column_types: [],
Expand All @@ -299,6 +301,7 @@ SpreadsheetArchitect.default_options = {
column_styles: [],
range_styles: [],
conditional_row_styles: [],
conditional_column_styles: [],
merges: [],
borders: [],
column_types: [],
Expand Down
30 changes: 24 additions & 6 deletions lib/spreadsheet_architect/class_methods/xlsx.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/spreadsheet_architect/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
38 changes: 34 additions & 4 deletions lib/spreadsheet_architect/utils/xlsx.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -196,18 +199,45 @@ 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

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
Expand Down
20 changes: 20 additions & 0 deletions test/unit/spreadsheet_architect/exceptions_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion test/unit/xlsx/general_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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"}},
Expand Down