excel_templating 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.rubocop.hound.yml +261 -0
- data/.rubocop.ph.yml +44 -0
- data/.rubocop.yml +3 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +8 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +3 -0
- data/README.md +133 -0
- data/Rakefile +43 -0
- data/excel_templating.gemspec +32 -0
- data/lib/excel_templating/document/data_source_registry/registry_list.rb +48 -0
- data/lib/excel_templating/document/data_source_registry/registry_renderer.rb +74 -0
- data/lib/excel_templating/document/data_source_registry.rb +64 -0
- data/lib/excel_templating/document/sheet/repeated_row.rb +39 -0
- data/lib/excel_templating/document/sheet.rb +133 -0
- data/lib/excel_templating/document.rb +71 -0
- data/lib/excel_templating/document_dsl.rb +85 -0
- data/lib/excel_templating/excel_abstraction/active_cell_reference.rb +59 -0
- data/lib/excel_templating/excel_abstraction/cell.rb +23 -0
- data/lib/excel_templating/excel_abstraction/cell_range.rb +26 -0
- data/lib/excel_templating/excel_abstraction/cell_reference.rb +39 -0
- data/lib/excel_templating/excel_abstraction/date.rb +36 -0
- data/lib/excel_templating/excel_abstraction/row.rb +29 -0
- data/lib/excel_templating/excel_abstraction/sheet.rb +102 -0
- data/lib/excel_templating/excel_abstraction/spread_sheet.rb +28 -0
- data/lib/excel_templating/excel_abstraction/time.rb +42 -0
- data/lib/excel_templating/excel_abstraction/work_book.rb +47 -0
- data/lib/excel_templating/excel_abstraction.rb +16 -0
- data/lib/excel_templating/render_helper.rb +14 -0
- data/lib/excel_templating/renderer.rb +251 -0
- data/lib/excel_templating/rspec_excel_matcher.rb +129 -0
- data/lib/excel_templating/version.rb +4 -0
- data/lib/excel_templating.rb +4 -0
- data/spec/assets/alphalist_7_4.mustache.xlsx +0 -0
- data/spec/assets/alphalist_seven_four_expected.xlsx +0 -0
- data/spec/assets/valid_cell.mustache.xlsx +0 -0
- data/spec/assets/valid_cell_expected.xlsx +0 -0
- data/spec/assets/valid_cell_expected_inline.xlsx +0 -0
- data/spec/assets/valid_column_expected.xlsx +0 -0
- data/spec/cell_validation_spec.rb +114 -0
- data/spec/column_validation_spec.rb +47 -0
- data/spec/excel_abstraction/active_cell_reference_spec.rb +73 -0
- data/spec/excel_abstraction/cell_range_spec.rb +36 -0
- data/spec/excel_abstraction/cell_reference_spec.rb +69 -0
- data/spec/excel_abstraction/cell_spec.rb +54 -0
- data/spec/excel_abstraction/date_spec.rb +27 -0
- data/spec/excel_abstraction/row_spec.rb +42 -0
- data/spec/excel_abstraction/sheet_spec.rb +83 -0
- data/spec/excel_abstraction/spread_sheet_spec.rb +35 -0
- data/spec/excel_abstraction/time_spec.rb +27 -0
- data/spec/excel_abstraction/work_book_spec.rb +22 -0
- data/spec/excel_helper.rb +16 -0
- data/spec/excel_templating_spec.rb +141 -0
- data/spec/spec_helper.rb +13 -0
- metadata +281 -0
@@ -0,0 +1,102 @@
|
|
1
|
+
module ExcelAbstraction
|
2
|
+
class Sheet < SimpleDelegator
|
3
|
+
attr_reader :active_cell_reference
|
4
|
+
|
5
|
+
def initialize(sheet, workbook)
|
6
|
+
super(sheet)
|
7
|
+
@workbook = workbook
|
8
|
+
@active_cell_reference = ExcelAbstraction::ActiveCellReference.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def header(value, options = {})
|
12
|
+
cell(value, {bold: 1}.merge(options))
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def headers(array, options = {})
|
17
|
+
Array(array).each { |element| header(element, options) }
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
# Fills the cell at the current pointer with the value and format or options specified
|
22
|
+
# @param [Object] value Value to place in the cell.
|
23
|
+
# @param [Object] options (optional) options to create a format object
|
24
|
+
# @param [Object] format (optional) The format to use when creating this cell
|
25
|
+
def cell(value, format: nil, type: :auto, **options)
|
26
|
+
format = format || _format(options)
|
27
|
+
value = Float(value) if value.is_a?(ExcelAbstraction::Time) || value.is_a?(ExcelAbstraction::Date)
|
28
|
+
|
29
|
+
writer = case type
|
30
|
+
when :string then method(:write_string)
|
31
|
+
else method(:write)
|
32
|
+
end
|
33
|
+
|
34
|
+
writer.call(active_cell_reference.row, active_cell_reference.col, value, format)
|
35
|
+
|
36
|
+
if options[:new_row]
|
37
|
+
next_row
|
38
|
+
else
|
39
|
+
active_cell_reference.right
|
40
|
+
end
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
# Advance the pointer to the next row.
|
45
|
+
# @return nil
|
46
|
+
def next_row
|
47
|
+
active_cell_reference.newline
|
48
|
+
end
|
49
|
+
|
50
|
+
def cells(array, options = {})
|
51
|
+
Array(array).each { |value| cell(value, options) }
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def merge(length, value, options = {})
|
56
|
+
format = _format(options)
|
57
|
+
merge_range(active_cell_reference.row, active_cell_reference.col, active_cell_reference.row, active_cell_reference.col + length, value, format)
|
58
|
+
active_cell_reference.right(length + 1)
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
def style_row(row, properties = {})
|
63
|
+
height = properties[:height]
|
64
|
+
format = _format(properties[:format] || {})
|
65
|
+
hidden = properties[:hidden] || 0
|
66
|
+
outline_level = properties[:level] || 0
|
67
|
+
if outline_level
|
68
|
+
raise "Outline level can only be between 0 and 7" unless (0..7) === outline_level
|
69
|
+
end
|
70
|
+
collapse = properties[:collapse] || 0
|
71
|
+
set_row(row, height, format, hidden, outline_level, collapse)
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
# Style the numbered column
|
76
|
+
# @param [Integer] col 0 based index of the column.
|
77
|
+
# @param [Integer] width numeric width of the column (30 = 2.6 inches)
|
78
|
+
# @param [Hash] format Properties to set for the format of the column
|
79
|
+
# @param [Integer] level Outline level
|
80
|
+
# @param [Integer] collapse 1 = collapse, 0 = do not collapse
|
81
|
+
def style_col(col, width: nil, format: {}, level: 0, collapse: 0, hidden: nil)
|
82
|
+
format = _format(format || {})
|
83
|
+
if level
|
84
|
+
raise "Outline level can only be between 0 and 7" unless (0..7) === level
|
85
|
+
end
|
86
|
+
set_column(col, col, width, format, hidden, level, collapse)
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def _format(options)
|
93
|
+
format = @workbook.add_format
|
94
|
+
format.set_align('valign')
|
95
|
+
options.each do |key, value|
|
96
|
+
method = "set_#{key.to_s}".to_sym
|
97
|
+
format.send(method, value) if format.respond_to?(method)
|
98
|
+
end
|
99
|
+
format
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module ExcelAbstraction
|
4
|
+
class SpreadSheet < DelegateClass(Tempfile)
|
5
|
+
attr_reader :workbook
|
6
|
+
|
7
|
+
def initialize(format: :xls, skip_default_sheet: false)
|
8
|
+
extension = format == :xls ? ".xls" : ".xlsx"
|
9
|
+
tmp_file = Tempfile.new(["temp_spreadsheet_#{::Time.now.to_i}", extension])
|
10
|
+
super(tmp_file)
|
11
|
+
@workbook = ExcelAbstraction::WorkBook.new(tmp_file.path, format: format, skip_default_sheet: skip_default_sheet)
|
12
|
+
end
|
13
|
+
|
14
|
+
def close
|
15
|
+
workbook.close
|
16
|
+
yield if block_given?
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
data = nil
|
22
|
+
close do
|
23
|
+
data = read
|
24
|
+
end
|
25
|
+
data
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ExcelAbstraction
|
2
|
+
class Time < SimpleDelegator
|
3
|
+
ADJUSTMENT = "1900-03-01 00:00 +00:00"
|
4
|
+
REFERENCE = "1900-01-01 00:00 +00:00"
|
5
|
+
|
6
|
+
attr_reader :value
|
7
|
+
|
8
|
+
def initialize(raw_value)
|
9
|
+
super(convert(raw_value))
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_excel_time
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def adjustment
|
19
|
+
::Time.parse(ADJUSTMENT)
|
20
|
+
end
|
21
|
+
|
22
|
+
def reference
|
23
|
+
::Time.parse(REFERENCE)
|
24
|
+
end
|
25
|
+
|
26
|
+
def adjust(raw_value)
|
27
|
+
adjustment < raw_value ? two_days : one_day
|
28
|
+
end
|
29
|
+
|
30
|
+
def two_days
|
31
|
+
one_day * 2.0
|
32
|
+
end
|
33
|
+
|
34
|
+
def one_day
|
35
|
+
60 * 60 * 24.to_f
|
36
|
+
end
|
37
|
+
|
38
|
+
def convert(raw_value)
|
39
|
+
(raw_value - reference + adjust(raw_value)).to_f / one_day
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module ExcelAbstraction
|
2
|
+
class WorkBook < SimpleDelegator
|
3
|
+
attr_accessor :active_sheet
|
4
|
+
|
5
|
+
def initialize(file, format: :xls, skip_default_sheet: false)
|
6
|
+
@format = format
|
7
|
+
@file = file
|
8
|
+
super(workbook)
|
9
|
+
unless skip_default_sheet
|
10
|
+
@active_sheet = ExcelAbstraction::Sheet.new(workbook.add_worksheet, workbook)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def title(text)
|
16
|
+
set_properties(title: text)
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def organization(name)
|
21
|
+
set_properties(company: name)
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :format, :file
|
28
|
+
|
29
|
+
def default_options
|
30
|
+
{
|
31
|
+
:font => 'Calibri',
|
32
|
+
:size => 12,
|
33
|
+
:align => 'center',
|
34
|
+
:text_wrap => 1
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def workbook
|
39
|
+
@workbook ||= if format == :xlsx
|
40
|
+
WriteXLSX.new(file, default_options)
|
41
|
+
else
|
42
|
+
WriteExcel.new(file, default_options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'write_xlsx'
|
2
|
+
require 'writeexcel'
|
3
|
+
|
4
|
+
require_relative 'excel_abstraction/active_cell_reference'
|
5
|
+
require_relative 'excel_abstraction/cell'
|
6
|
+
require_relative 'excel_abstraction/cell_range'
|
7
|
+
require_relative 'excel_abstraction/cell_reference'
|
8
|
+
require_relative 'excel_abstraction/date'
|
9
|
+
require_relative 'excel_abstraction/row'
|
10
|
+
require_relative 'excel_abstraction/sheet'
|
11
|
+
require_relative 'excel_abstraction/spread_sheet'
|
12
|
+
require_relative 'excel_abstraction/time'
|
13
|
+
require_relative 'excel_abstraction/work_book'
|
14
|
+
|
15
|
+
module ExcelAbstraction
|
16
|
+
end
|
@@ -0,0 +1,251 @@
|
|
1
|
+
require_relative 'excel_abstraction'
|
2
|
+
require 'mustache'
|
3
|
+
|
4
|
+
module ExcelTemplating
|
5
|
+
# Render class for ExcelTemplating Documents. Used by the Document to render the defined document
|
6
|
+
# with the data to a new file. Responsible for reading the template and applying the data to it
|
7
|
+
class Renderer
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
# @param [ExcelTemplating::Document] document Document to render with
|
11
|
+
def initialize(document)
|
12
|
+
@template_document = document
|
13
|
+
@data_source_registry = document.class.data_source_registry
|
14
|
+
end
|
15
|
+
|
16
|
+
# Render the document provided. Yields the path to the tempfile created.
|
17
|
+
def render
|
18
|
+
@spreadsheet = ExcelAbstraction::SpreadSheet.new(format: :xlsx)
|
19
|
+
@template = Roo::Spreadsheet.open(template_path)
|
20
|
+
@registry_renderer = data_source_registry.renderer(data: data[:all_sheets])
|
21
|
+
apply_document_level_items
|
22
|
+
apply_data_to_sheets
|
23
|
+
protect_spreadsheet
|
24
|
+
registry_renderer.write_sheet(@spreadsheet.workbook)
|
25
|
+
|
26
|
+
@spreadsheet.close
|
27
|
+
yield(spreadsheet.path)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :template_document, :spreadsheet, :template, :registry_renderer, :data_source_registry, :current_sheet
|
33
|
+
delegate [:workbook] => :spreadsheet
|
34
|
+
delegate [:data] => :template_document
|
35
|
+
delegate [:active_sheet] => :workbook
|
36
|
+
delegate [:active_cell_reference] => :active_sheet
|
37
|
+
|
38
|
+
def current_row
|
39
|
+
active_cell_reference.row
|
40
|
+
end
|
41
|
+
|
42
|
+
def current_col
|
43
|
+
active_cell_reference.col
|
44
|
+
end
|
45
|
+
|
46
|
+
def template_path
|
47
|
+
template_document.class.template_path
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_format_styling
|
51
|
+
template_document.class.document_default_styling
|
52
|
+
end
|
53
|
+
|
54
|
+
def protected?
|
55
|
+
template_document.class.protected?
|
56
|
+
end
|
57
|
+
|
58
|
+
def sheets
|
59
|
+
template_document.class.sheets
|
60
|
+
end
|
61
|
+
|
62
|
+
def apply_document_level_items
|
63
|
+
workbook.title mustachify(template_document.class.document_title, locals: common_data_variables)
|
64
|
+
workbook.organization mustachify(template_document.class.document_organization, locals: common_data_variables)
|
65
|
+
end
|
66
|
+
|
67
|
+
def common_data_variables
|
68
|
+
stringify_keys(data[:all_sheets])
|
69
|
+
end
|
70
|
+
|
71
|
+
def stringify_keys(hash)
|
72
|
+
Hash[hash.map { |k, v| [k.to_s, v] } ]
|
73
|
+
end
|
74
|
+
|
75
|
+
def style_columns(sheet, template_sheet)
|
76
|
+
default_style = sheet.default_column_style
|
77
|
+
column_styles = sheet.column_styles
|
78
|
+
if column_styles || default_style
|
79
|
+
roo_columns(template_sheet).each do |column_number|
|
80
|
+
style = column_styles[column_number] || default_style
|
81
|
+
active_sheet.style_col(column_number - 1, style) # Note: Styling columns is zero indexed
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def style_rows(sheet, template_sheet)
|
87
|
+
default_style = sheet.default_row_style
|
88
|
+
row_styles = sheet.row_styles
|
89
|
+
if row_styles || default_style
|
90
|
+
roo_rows(template_sheet).each do |row_number|
|
91
|
+
style = row_styles[row_number] || default_style
|
92
|
+
active_sheet.style_row(row_number - 1, style) # Note: Styling rows is zero indexed
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def roo_columns(roo_sheet)
|
98
|
+
(roo_sheet.first_column .. roo_sheet.last_column)
|
99
|
+
end
|
100
|
+
|
101
|
+
def roo_rows(roo_sheet)
|
102
|
+
(roo_sheet.first_row .. roo_sheet.last_row)
|
103
|
+
end
|
104
|
+
|
105
|
+
def protect_spreadsheet
|
106
|
+
active_sheet.protect if protected?
|
107
|
+
end
|
108
|
+
|
109
|
+
def apply_data_to_sheets
|
110
|
+
sheets.each_with_index do |sheet, sheet_number|
|
111
|
+
@current_sheet = sheet
|
112
|
+
sheet_data = sheet.sheet_data(data)
|
113
|
+
template_sheet = template.sheet(sheet_number)
|
114
|
+
|
115
|
+
# column and row styles should be applied before writing any data to the sheet
|
116
|
+
style_columns(sheet, template_sheet)
|
117
|
+
# row styles have priority over column styles
|
118
|
+
style_rows(sheet, template_sheet)
|
119
|
+
|
120
|
+
roo_rows(template_sheet).each do |row_number|
|
121
|
+
sheet.each_row_at(row_number, sheet_data) do |row_data|
|
122
|
+
local_data = stringify_keys(data[:all_sheets]).merge(stringify_keys(row_data))
|
123
|
+
roo_columns(template_sheet).each do |column_number|
|
124
|
+
apply_data_to_cell(local_data, template_sheet, row_number, column_number)
|
125
|
+
if sheet.validated_cell?(row_number, column_number)
|
126
|
+
add_validation(sheet, row_number, column_number)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
active_sheet.next_row
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def apply_data_to_cell(local_data, template_sheet, row_number, column_number)
|
136
|
+
template_cell = template_sheet.cell(row_number, column_number)
|
137
|
+
font = template_sheet.font(row_number, column_number)
|
138
|
+
format = format_for(font, row_number, column_number)
|
139
|
+
value = mustachify(template_cell, locals: local_data)
|
140
|
+
|
141
|
+
active_sheet.cell(
|
142
|
+
value,
|
143
|
+
type: type_for_value(value),
|
144
|
+
format: format
|
145
|
+
)
|
146
|
+
end
|
147
|
+
|
148
|
+
def type_for_value(value)
|
149
|
+
looks_like_a_separator?(value) ? :string : :auto
|
150
|
+
end
|
151
|
+
|
152
|
+
def looks_like_a_separator?(value)
|
153
|
+
value.is_a?(String) && value =~ /^==/
|
154
|
+
end
|
155
|
+
|
156
|
+
def format_for(font, row_number, column_number)
|
157
|
+
format = font_formats(font)
|
158
|
+
|
159
|
+
set_column_lock(format, column_number) if column_is_locked?(column_number)
|
160
|
+
set_row_lock(format, row_number) if row_is_locked?(row_number)
|
161
|
+
|
162
|
+
format
|
163
|
+
end
|
164
|
+
|
165
|
+
def column_is_locked?(column_number)
|
166
|
+
row_or_column_is_locked? current_sheet.column_styles[column_number]
|
167
|
+
end
|
168
|
+
|
169
|
+
def row_is_locked?(row_number)
|
170
|
+
row_or_column_is_locked? current_sheet.row_styles[row_number]
|
171
|
+
end
|
172
|
+
|
173
|
+
def row_or_column_is_locked?(row_or_column)
|
174
|
+
row_or_column && row_or_column[:format] && row_or_column[:format].has_key?(:locked)
|
175
|
+
end
|
176
|
+
|
177
|
+
def row_or_column_lock_attribute(row_or_column)
|
178
|
+
row_or_column && row_or_column[:format] && row_or_column[:format][:locked]
|
179
|
+
end
|
180
|
+
|
181
|
+
def set_row_lock(format, row_number)
|
182
|
+
format.set_locked row_or_column_lock_attribute(current_sheet.row_styles[row_number])
|
183
|
+
end
|
184
|
+
|
185
|
+
def set_column_lock(format, column_number)
|
186
|
+
format.set_locked row_or_column_lock_attribute(current_sheet.column_styles[column_number])
|
187
|
+
end
|
188
|
+
|
189
|
+
def font_formats(font=nil)
|
190
|
+
template_font = font || Roo::Font.new
|
191
|
+
|
192
|
+
format_details = {
|
193
|
+
bold: template_font.bold? ? 1 : 0,
|
194
|
+
italic: template_font.italic? ? 1 : 0,
|
195
|
+
underline: template_font.underline? ? 1 : 0
|
196
|
+
}.merge(default_format_styling)
|
197
|
+
|
198
|
+
workbook.add_format format_details
|
199
|
+
end
|
200
|
+
|
201
|
+
def mustachify(inline_template, locals: {})
|
202
|
+
if whole_cell_template?(inline_template)
|
203
|
+
locals.fetch(extract_key_from_template(inline_template))
|
204
|
+
elsif no_mustache_found?(inline_template)
|
205
|
+
inline_template
|
206
|
+
else
|
207
|
+
MustacheRenderer.render(inline_template, locals)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def no_mustache_found?(value)
|
212
|
+
!value.is_a?(String) || !(value.match(/{{.+}}/))
|
213
|
+
end
|
214
|
+
|
215
|
+
def extract_key_from_template(template)
|
216
|
+
template[whole_cell_template_matcher, 1]
|
217
|
+
end
|
218
|
+
|
219
|
+
def whole_cell_template_matcher
|
220
|
+
/^{{([^{}]+)}}$/
|
221
|
+
end
|
222
|
+
|
223
|
+
def whole_cell_template?(template)
|
224
|
+
template =~ whole_cell_template_matcher
|
225
|
+
end
|
226
|
+
|
227
|
+
def add_validation(sheet, row_number, column_number)
|
228
|
+
raise ArgumentError, "No :data_sources defined for validation!" unless data_source_registry
|
229
|
+
source = sheet.validation_source_name(row_number, column_number)
|
230
|
+
#Use current_row and current_col here because row_number and column_number refer to the template
|
231
|
+
#sheet and we want to write a reference to the cell we just wrote
|
232
|
+
active_sheet.data_validation absolute_reference(current_row + 1, current_col),
|
233
|
+
registry_renderer.absolute_reference_for(source)
|
234
|
+
end
|
235
|
+
|
236
|
+
def absolute_reference(row_number, column_number)
|
237
|
+
"$#{RenderHelper.letter_for(column_number)}$#{row_number}"
|
238
|
+
end
|
239
|
+
|
240
|
+
class MustacheRenderer < Mustache
|
241
|
+
|
242
|
+
def self.render(template, locals)
|
243
|
+
renderer = new
|
244
|
+
renderer.template = template
|
245
|
+
renderer.raise_on_context_miss = true
|
246
|
+
renderer.render(locals)
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'roo'
|
2
|
+
require "roo-xls"
|
3
|
+
|
4
|
+
# Matcher for rspec 'match_excel_content'
|
5
|
+
# @example
|
6
|
+
# expect do
|
7
|
+
# subject.render do |path|
|
8
|
+
# expect(path).to match_excel_content('spec/assets/spreadsheets/seven_three_expected.xlsx')
|
9
|
+
# end
|
10
|
+
# end
|
11
|
+
RSpec::Matchers.define :match_excel_content do |expected_excel_path|
|
12
|
+
match do |excel_path|
|
13
|
+
@excel_matcher = RSpec::Matchers::ExcelMatcher.new
|
14
|
+
@excel_matcher.expected_roo = Roo::Spreadsheet.open(expected_excel_path)
|
15
|
+
@excel_matcher.actual_roo = Roo::Spreadsheet.open(excel_path)
|
16
|
+
@excel_matcher.match?
|
17
|
+
end
|
18
|
+
|
19
|
+
failure_message do |excel_path|
|
20
|
+
messages = ["expected excel:#{expected_excel_path} to exactly match the content of #{excel_path} but did not."]
|
21
|
+
@excel_matcher.errors.group_by(&:first).each do |sheet_name, errors|
|
22
|
+
messages << "#{sheet_name}:"
|
23
|
+
errors.each do |_, error|
|
24
|
+
messages << " #{error}:"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
messages.join("\n")
|
28
|
+
end
|
29
|
+
|
30
|
+
failure_message_when_negated do |excel_path|
|
31
|
+
"expected excel:#{expected_excel_path} to not exactly match the content of #{excel_path} but it did."
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Specific Matcher class helper for comparing two excel documents.
|
36
|
+
class RSpec::Matchers::ExcelMatcher
|
37
|
+
|
38
|
+
def initialize
|
39
|
+
@errors = []
|
40
|
+
end
|
41
|
+
|
42
|
+
def match?
|
43
|
+
sheet_count_equal? && all_sheets_equal?
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_accessor :errors, :expected_roo, :actual_roo
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def check(sheet_name, check_result, fail_msg)
|
51
|
+
if check_result
|
52
|
+
true
|
53
|
+
else
|
54
|
+
errors << [sheet_name, fail_msg]
|
55
|
+
false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def sheet_count_equal?
|
60
|
+
check(:base, expected_roo.sheets.count == actual_roo.sheets.count, "Number of sheets do not match.")
|
61
|
+
end
|
62
|
+
|
63
|
+
def all_sheets_equal?
|
64
|
+
sheets(expected_roo).zip(sheets(actual_roo)).all? do |expected_sheet, actual_sheet|
|
65
|
+
sheets_equal?(expected_sheet, actual_sheet)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def sheets(roo)
|
70
|
+
(0 .. (roo.sheets.count - 1)).map do |i|
|
71
|
+
roo.sheet(i)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def sheets_equal?(expected_workbook, actual_workbook)
|
76
|
+
expected_workbook.sheets.all? do |sheet_name|
|
77
|
+
expected = expected_workbook.sheet(sheet_name)
|
78
|
+
actual = begin
|
79
|
+
actual_workbook.sheet(sheet_name)
|
80
|
+
rescue
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
check(sheet_name, actual, "Sheet names do not match.") if actual != nil
|
85
|
+
# check(sheet_name, expected.first_row == actual.first_row, "Number of rows do not match.")
|
86
|
+
# check(sheet_name, expected.last_row == actual.last_row, "Number of rows do not match")
|
87
|
+
# check(sheet_name, expected.first_column == actual.first_column, "Number of columns do not match")
|
88
|
+
# check(sheet_name, expected.last_column == actual.last_column, "Number of columns do not match")
|
89
|
+
check_cells(sheet_name, expected, actual)
|
90
|
+
|
91
|
+
errors.empty?
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def check_cells(sheet_name, sheet1, sheet2)
|
96
|
+
(sheet1.first_row .. sheet1.last_row).each do |row|
|
97
|
+
(sheet1.first_column .. sheet1.last_column).each do |col|
|
98
|
+
check(sheet_name, compare_value(col, row, sheet1, sheet2), discrepancy_message(col, row, sheet1, sheet2))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def compare_value(col, row, sheet1, sheet2)
|
104
|
+
value2 = sheet2.cell(row, col)
|
105
|
+
value1 = sheet1.cell(row, col)
|
106
|
+
if value2.class == Float
|
107
|
+
value2 == value1.to_f
|
108
|
+
elsif value1.class == Float
|
109
|
+
value1 == value2.to_f
|
110
|
+
else
|
111
|
+
value2 == value1
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def discrepancy_message(col, row, sheet1, sheet2)
|
116
|
+
cell_id = "#{column_letter(col)}#{row}"
|
117
|
+
actual_value = sheet2.cell(row, col).inspect
|
118
|
+
expected_value = sheet1.cell(row, col).inspect
|
119
|
+
"Cell #{cell_id} actual:#{actual_value} expected:#{expected_value}"
|
120
|
+
end
|
121
|
+
|
122
|
+
def column_letter(col_number)
|
123
|
+
column_letters[col_number - 1]
|
124
|
+
end
|
125
|
+
|
126
|
+
def column_letters
|
127
|
+
@column_letters ||= ('A' .. 'ZZ').to_a
|
128
|
+
end
|
129
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|