ruport 0.7.2 → 0.8.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.
- data/AUTHORS +7 -3
- data/Rakefile +8 -9
- data/TODO +16 -0
- data/examples/RWEmerson.jpg +0 -0
- data/examples/centered_pdf_text_box.rb +66 -0
- data/examples/invoice.rb +35 -25
- data/examples/invoice_report.rb +1 -1
- data/examples/line_plotter.rb +1 -1
- data/examples/pdf_table_with_title.rb +42 -0
- data/lib/ruport.rb +5 -7
- data/lib/ruport.rb.rej +41 -0
- data/lib/ruport.rb~ +85 -0
- data/lib/ruport/attempt.rb +59 -59
- data/lib/ruport/config.rb +15 -4
- data/lib/ruport/data.rb +0 -2
- data/lib/ruport/data/groupable.rb +25 -16
- data/lib/ruport/data/record.rb +128 -102
- data/lib/ruport/data/table.rb +352 -199
- data/lib/ruport/data/taggable.rb +18 -7
- data/lib/ruport/format/html.rb +3 -1
- data/lib/ruport/format/latex.rb +1 -1
- data/lib/ruport/format/latex.rb.rej +26 -0
- data/lib/ruport/format/latex.rb~ +47 -0
- data/lib/ruport/format/pdf.rb +111 -28
- data/lib/ruport/format/pdf.rb.rej +168 -0
- data/lib/ruport/format/pdf.rb~ +189 -0
- data/lib/ruport/format/plugin.rb +0 -5
- data/lib/ruport/format/svg.rb +4 -4
- data/lib/ruport/format/xml.rb +3 -3
- data/lib/ruport/generator.rb +66 -27
- data/lib/ruport/mailer.rb +4 -1
- data/lib/ruport/query.rb +13 -1
- data/lib/ruport/renderer.rb +89 -17
- data/lib/ruport/renderer/graph.rb +5 -5
- data/lib/ruport/renderer/table.rb +8 -9
- data/lib/ruport/report.rb +2 -6
- data/test/test_config.rb +88 -76
- data/test/{test_text_table.rb → test_format_text.rb} +4 -2
- data/test/test_groupable.rb +15 -13
- data/test/test_query.rb +6 -3
- data/test/test_record.rb +57 -33
- data/test/test_renderer.rb +77 -0
- data/test/test_report.rb +188 -181
- data/test/test_ruport.rb +5 -6
- data/test/test_table.rb +290 -190
- data/test/test_table_renderer.rb +56 -8
- data/test/test_taggable.rb +7 -8
- data/test/unit.log +259 -7
- metadata +22 -19
- data/lib/ruport/data/collection.rb +0 -65
- data/lib/ruport/data/set.rb +0 -148
- data/test/test_collection.rb +0 -30
- data/test/test_set.rb +0 -118
data/lib/ruport/data/taggable.rb
CHANGED
@@ -5,7 +5,8 @@
|
|
5
5
|
# Copyright 2006 by respective content owners, all rights reserved.
|
6
6
|
module Ruport::Data
|
7
7
|
|
8
|
-
|
8
|
+
require 'set'
|
9
|
+
#
|
9
10
|
# === Overview
|
10
11
|
#
|
11
12
|
# This module provides a simple mechanism for tagging arbitrary objects.
|
@@ -14,6 +15,7 @@ module Ruport::Data
|
|
14
15
|
#
|
15
16
|
module Taggable
|
16
17
|
|
18
|
+
#
|
17
19
|
# Adds a tag to the object.
|
18
20
|
#
|
19
21
|
# Example:
|
@@ -21,9 +23,10 @@ module Ruport::Data
|
|
21
23
|
# taggable_obj.tag :spiffy
|
22
24
|
#
|
23
25
|
def tag(tag_name)
|
24
|
-
tags << tag_name
|
26
|
+
tags << tag_name
|
25
27
|
end
|
26
28
|
|
29
|
+
#
|
27
30
|
# Removes a tag from the object.
|
28
31
|
#
|
29
32
|
# Example:
|
@@ -34,6 +37,7 @@ module Ruport::Data
|
|
34
37
|
tags.delete tag_name
|
35
38
|
end
|
36
39
|
|
40
|
+
#
|
37
41
|
# Checks to see if a tag is present.
|
38
42
|
#
|
39
43
|
# Example:
|
@@ -44,24 +48,31 @@ module Ruport::Data
|
|
44
48
|
tags.include? tag_name
|
45
49
|
end
|
46
50
|
|
51
|
+
#
|
47
52
|
# Returns an Array of the object's tags.
|
48
53
|
#
|
49
54
|
# Example:
|
50
55
|
#
|
51
56
|
# taggable_obj.tags #=> [:spiffy, :kind_of_spiffy]
|
52
57
|
#
|
53
|
-
def tags
|
54
|
-
@ruport_tags ||=
|
58
|
+
def tags
|
59
|
+
@ruport_tags ||= Set.new
|
55
60
|
end
|
56
61
|
|
57
|
-
#
|
62
|
+
#
|
63
|
+
# Sets the tags.
|
58
64
|
#
|
59
65
|
# Example:
|
60
66
|
#
|
61
67
|
# taggable_obj.tags = [:really_dang_spiffy, :the_most_spiffy]
|
62
68
|
#
|
63
|
-
def tags=(tags_list)
|
64
|
-
|
69
|
+
def tags=(tags_list)
|
70
|
+
case tags_list
|
71
|
+
when Array
|
72
|
+
@ruport_tags = Set.new(tags_list)
|
73
|
+
else
|
74
|
+
@ruport_tags = tags_list
|
75
|
+
end
|
65
76
|
end
|
66
77
|
|
67
78
|
end
|
data/lib/ruport/format/html.rb
CHANGED
@@ -24,7 +24,9 @@ module Ruport::Format
|
|
24
24
|
def build_table_body
|
25
25
|
output << data.inject("") do |s,r|
|
26
26
|
row = r.map { |e| e.to_s.empty? ? " " : e }
|
27
|
-
classstr = r.tags.inject("") {|cs,c| cs + " class='#{c}'" }
|
27
|
+
#classstr = r.tags.inject("") {|cs,c| cs + " class='#{c}'" }
|
28
|
+
classstr =
|
29
|
+
r.tags.length > 0 ? " class='#{r.tags.to_a.join(' ')}'" : ""
|
28
30
|
s + "\t\t<tr#{classstr}>\n\t\t\t<td#{classstr}>" +
|
29
31
|
row.to_a.join("</td>\n\t\t\t<td#{classstr}>") +
|
30
32
|
"</td>\n\t\t</tr>\n"
|
data/lib/ruport/format/latex.rb
CHANGED
@@ -0,0 +1,26 @@
|
|
1
|
+
***************
|
2
|
+
*** 15,27 ****
|
3
|
+
output << " }\n"
|
4
|
+
output << "\\hline\n"
|
5
|
+
|
6
|
+
- #FIXME: this ain't ruby, jh ;)
|
7
|
+
- counter = 0
|
8
|
+
-
|
9
|
+
- data.column_names.each do |t|
|
10
|
+
- output << " & " unless counter == 0
|
11
|
+
output << "\\textsc{#{t}}"
|
12
|
+
- counter += 1
|
13
|
+
end
|
14
|
+
|
15
|
+
output << "\\\\\n"
|
16
|
+
--- 15,24 ----
|
17
|
+
output << " }\n"
|
18
|
+
output << "\\hline\n"
|
19
|
+
|
20
|
+
+ output << "\\textsc{#{data.column_names[0]}}"
|
21
|
+
+ data.column_names[1..-1].each do |t|
|
22
|
+
+ output << " & "
|
23
|
+
output << "\\textsc{#{t}}"
|
24
|
+
end
|
25
|
+
|
26
|
+
output << "\\\\\n"
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Ruport::Format
|
2
|
+
class Latex < Plugin
|
3
|
+
|
4
|
+
attr_accessor :caption
|
5
|
+
|
6
|
+
def build_table_header
|
7
|
+
output << "\\documentclass[11pt]{article}\n" <<
|
8
|
+
"\\RequirePackage{lscape,longtable}\n" <<
|
9
|
+
"\\begin{document}\n" <<
|
10
|
+
"\\begin{longtable}[c]{ "
|
11
|
+
|
12
|
+
data.column_names.each do
|
13
|
+
output << " p{2cm} "
|
14
|
+
end
|
15
|
+
output << " }\n"
|
16
|
+
output << "\\hline\n"
|
17
|
+
|
18
|
+
output << "\\textsc{#{data.column_names[0]}}"
|
19
|
+
data.column_names[1..-1].each do |t|
|
20
|
+
output << " & "
|
21
|
+
output << "\\textsc{#{t}}"
|
22
|
+
end
|
23
|
+
|
24
|
+
output << "\\\\\n"
|
25
|
+
output << "\\hline\n"
|
26
|
+
output << "\\endhead\n"
|
27
|
+
output << "\\endfoot\n"
|
28
|
+
output << "\\hline\n"
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_table_body
|
32
|
+
data.each do |r|
|
33
|
+
output << r.to_a.join(" & ") + "\\\\\n"
|
34
|
+
output << "\\hline\n"
|
35
|
+
end
|
36
|
+
if caption
|
37
|
+
output << "\\caption[#{caption}]{#{caption}}\n"
|
38
|
+
end
|
39
|
+
output << "\\end{longtable}\n"
|
40
|
+
end
|
41
|
+
|
42
|
+
def build_table_footer
|
43
|
+
output << "\\end{document}\n"
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
data/lib/ruport/format/pdf.rb
CHANGED
@@ -11,10 +11,6 @@ module Ruport::Format
|
|
11
11
|
# * table_width
|
12
12
|
# * max_table_width #=> 500
|
13
13
|
#
|
14
|
-
# This class makes extensive use of Austin Zeigler's PDF::Writer
|
15
|
-
# Please refer to the API documentation for PDF::Writer if you need more
|
16
|
-
# information.
|
17
|
-
#
|
18
14
|
class PDF < Plugin
|
19
15
|
attr_writer :pdf_writer
|
20
16
|
attr_accessor :table_header_proc
|
@@ -77,45 +73,132 @@ module Ruport::Format
|
|
77
73
|
def add_text(*args)
|
78
74
|
pdf_writer.text(*args)
|
79
75
|
end
|
76
|
+
|
77
|
+
# - if the image is bigger than the box, it will be scaled down until it fits
|
78
|
+
# - if the image is smaller than the box it's won't be resized
|
79
|
+
#
|
80
|
+
# arguments:
|
81
|
+
# - x: left bound of box
|
82
|
+
# - y: bottom bound of box
|
83
|
+
# - width: width of box
|
84
|
+
# - height: height of box
|
85
|
+
def center_image_in_box(path, x, y, width, height)
|
86
|
+
info = ::PDF::Writer::Graphics::ImageInfo.new(File.read(path))
|
87
|
+
|
88
|
+
# if the image is larger than the requested box, prepare to
|
89
|
+
# scale it down
|
90
|
+
fits = !(info.width > width || info.height > height)
|
91
|
+
|
92
|
+
# setup initial sizes for the image. These will be reduced as necesary
|
93
|
+
img_width = info.width
|
94
|
+
img_height = info.height
|
95
|
+
img_ratio = info.height.to_f / info.width.to_f
|
96
|
+
|
97
|
+
# reduce the size of the image until it fits into the requested box
|
98
|
+
until fits
|
99
|
+
img_width -= 1
|
100
|
+
img_height = img_width * img_ratio
|
101
|
+
fits = true if img_width < width && img_height < height
|
102
|
+
end
|
103
|
+
|
104
|
+
# if the width of the image is less than the requested box, calculate
|
105
|
+
# the white space buffer
|
106
|
+
if img_width < width
|
107
|
+
white_space = width - img_width
|
108
|
+
x = x + (white_space / 2)
|
109
|
+
end
|
110
|
+
|
111
|
+
# if the height of the image is less than the requested box, calculate
|
112
|
+
# the white space buffer
|
113
|
+
if img_height < height
|
114
|
+
white_space = height - img_height
|
115
|
+
y = y + (white_space / 2)
|
116
|
+
end
|
117
|
+
|
118
|
+
pdf_writer.add_image_from_file(path, x, y, img_width, img_height)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Draws some text on the canvas, surrounded by a box with rounded corners
|
122
|
+
#
|
123
|
+
def rounded_text_box(text)
|
124
|
+
opts = OpenStruct.new
|
125
|
+
yield(opts)
|
126
|
+
|
127
|
+
# resize the text automatically to ensure it isn't wider than the box
|
128
|
+
loop do
|
129
|
+
sz = pdf_writer.text_width( text, opts.font_size )
|
130
|
+
opts.x + sz > opts.x + opts.width or break
|
131
|
+
opts.font_size -= 1
|
132
|
+
end
|
133
|
+
|
134
|
+
# save the drawing state (colors, etc) so we can restore it later
|
135
|
+
pdf_writer.save_state
|
136
|
+
|
137
|
+
# draw our box
|
138
|
+
pdf_writer.fill_color(opts.fill_color || Color::RGB::White)
|
139
|
+
pdf_writer.stroke_color(opts.stroke_color || Color::RGB::Black)
|
140
|
+
pdf_writer.rounded_rectangle( opts.x, opts.y,
|
141
|
+
opts.width, opts.height,
|
142
|
+
opts.radius).fill_stroke
|
143
|
+
|
144
|
+
# if a heading for this box has been specified
|
145
|
+
if opts.heading
|
146
|
+
pdf_writer.line( opts.x, opts.y - 20,
|
147
|
+
opts.x + opts.width, opts.y - 20).stroke
|
148
|
+
pdf_writer.fill_color(Color::RGB::Black)
|
149
|
+
move_cursor_to(opts.y - 3)
|
150
|
+
add_text("<b>#{opts.heading}</b>",
|
151
|
+
:absolute_left => opts.x, :absolute_right => opts.x + opts.width,
|
152
|
+
:justification => :center, :font_size => opts.font_size)
|
153
|
+
end
|
154
|
+
|
155
|
+
# restore the original colors
|
156
|
+
pdf_writer.restore_state
|
157
|
+
|
158
|
+
# move our y cursor into position, write the text, then move the cursor
|
159
|
+
# to be just below the box
|
160
|
+
pdf_writer.y = opts.heading ? opts.y - 20 : opts.y
|
161
|
+
|
162
|
+
add_text( text, :absolute_left => opts.x,
|
163
|
+
:absolute_right => opts.x + opts.width,
|
164
|
+
:justification => opts.justification || :center,
|
165
|
+
:font_size => opts.font_size )
|
166
|
+
|
167
|
+
pdf_writer.y = opts.y - opts.height
|
168
|
+
end
|
169
|
+
|
170
|
+
# adds an image to every page. The color and size won't be modified,
|
171
|
+
# but it will be centered.
|
172
|
+
#
|
173
|
+
def watermark(imgpath)
|
174
|
+
x = pdf_writer.absolute_left_margin
|
175
|
+
y = pdf_writer.absolute_bottom_margin
|
176
|
+
width = pdf_writer.absolute_right_margin - x
|
177
|
+
height = pdf_writer.absolute_top_margin - y
|
178
|
+
|
179
|
+
pdf_writer.open_object do |wm|
|
180
|
+
pdf_writer.save_state
|
181
|
+
center_image_in_box(imgpath, x, y, width, height)
|
182
|
+
pdf_writer.restore_state
|
183
|
+
pdf_writer.close_object
|
184
|
+
pdf_writer.add_object(wm, :all_pages)
|
185
|
+
end
|
186
|
+
end
|
80
187
|
|
81
|
-
# Adds n to PDF::Writer#y.
|
82
|
-
#
|
83
|
-
# Basically, this allows you to move the rendering cursor up and down the page.
|
84
|
-
#
|
85
|
-
# move_cursor(10) #=> move up 10
|
86
|
-
# move_cursor(-10) #=> move down 10
|
87
188
|
def move_cursor(n)
|
88
189
|
pdf_writer.y += n
|
89
190
|
end
|
90
191
|
|
91
|
-
# Sets PDF::Writer#y to n.
|
92
|
-
#
|
93
|
-
# This lets you move to a given height on the page.
|
94
|
-
#
|
95
192
|
def move_cursor_to(n)
|
96
193
|
pdf_writer.y = n
|
97
194
|
end
|
98
195
|
|
99
|
-
# creates a margin of <tt>y</tt> amount and allows a block to be called to
|
100
|
-
# render items within that margin.
|
101
|
-
#
|
102
|
-
# Example:
|
103
|
-
#
|
104
|
-
# pad(10) { add_text 'hello' } #=> creates text hello with vertical margin
|
105
|
-
# of 10.
|
106
196
|
def pad(y,&block)
|
107
197
|
move_cursor -y
|
108
198
|
block.call
|
109
199
|
move_cursor -y
|
110
200
|
end
|
111
201
|
|
112
|
-
# Builds a PDF::SimpleTable from a Data::Table
|
113
|
-
#
|
114
|
-
# Can be customized via the follow layout parameters:
|
115
|
-
#
|
116
|
-
# max_table_width #=> defaults to 500
|
117
|
-
# table_width #=> optionally set a fixed width for the table
|
118
|
-
# orientation #=> defaults to :center
|
119
202
|
def draw_table
|
120
203
|
m = "Sorry, cant build PDFs from array like things (yet)"
|
121
204
|
raise m if data.column_names.empty?
|
@@ -0,0 +1,168 @@
|
|
1
|
+
***************
|
2
|
+
*** 11,65 ****
|
3
|
+
# * table_width
|
4
|
+
# * max_table_width #=> 500
|
5
|
+
#
|
6
|
+
class PDF < Plugin
|
7
|
+
attr_writer :pdf_writer
|
8
|
+
attr_accessor :table_header_proc
|
9
|
+
attr_accessor :table_footer_proc
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
require "pdf/writer"
|
13
|
+
require "pdf/simpletable"
|
14
|
+
end
|
15
|
+
|
16
|
+
def pdf_writer
|
17
|
+
@pdf_writer ||=
|
18
|
+
::PDF::Writer.new( :paper => layout.paper_size || "LETTER" )
|
19
|
+
end
|
20
|
+
|
21
|
+
def build_table_header
|
22
|
+
table_header_proc[pdf_writer] if table_header_proc
|
23
|
+
end
|
24
|
+
|
25
|
+
def build_table_body
|
26
|
+
draw_table
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_table_footer
|
30
|
+
table_footer_proc[pdf_writer] if table_footer_proc
|
31
|
+
end
|
32
|
+
|
33
|
+
def finalize_table
|
34
|
+
output << pdf_writer.render
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_text(*args)
|
38
|
+
pdf_writer.text(*args)
|
39
|
+
end
|
40
|
+
|
41
|
+
def move_cursor(n)
|
42
|
+
pdf_writer.y += n
|
43
|
+
end
|
44
|
+
|
45
|
+
def move_cursor_to(n)
|
46
|
+
pdf_writer.y = n
|
47
|
+
end
|
48
|
+
|
49
|
+
def pad(y,&block)
|
50
|
+
move_cursor -y
|
51
|
+
block.call
|
52
|
+
move_cursor -y
|
53
|
+
end
|
54
|
+
|
55
|
+
def draw_table
|
56
|
+
m = "Sorry, cant build PDFs from array like things (yet)"
|
57
|
+
raise m if data.column_names.empty?
|
58
|
+
--- 11,120 ----
|
59
|
+
# * table_width
|
60
|
+
# * max_table_width #=> 500
|
61
|
+
#
|
62
|
+
+ # This class makes extensive use of Austin Zeigler's PDF::Writer
|
63
|
+
+ # Please refer to the API documentation for PDF::Writer if you need more
|
64
|
+
+ # information.
|
65
|
+
+ #
|
66
|
+
class PDF < Plugin
|
67
|
+
attr_writer :pdf_writer
|
68
|
+
attr_accessor :table_header_proc
|
69
|
+
attr_accessor :table_footer_proc
|
70
|
+
|
71
|
+
+ # Does the necessary PDF::Writer requires
|
72
|
+
def initialize
|
73
|
+
require "pdf/writer"
|
74
|
+
require "pdf/simpletable"
|
75
|
+
end
|
76
|
+
|
77
|
+
+ # Returns the current PDF::Writer object or creates a new one if it has not
|
78
|
+
+ # been set yet.
|
79
|
+
+ #
|
80
|
+
def pdf_writer
|
81
|
+
@pdf_writer ||=
|
82
|
+
::PDF::Writer.new( :paper => layout.paper_size || "LETTER" )
|
83
|
+
end
|
84
|
+
|
85
|
+
+ # If table_header_proc is defined, it will be executed and the PDF::Writer
|
86
|
+
+ # object will be yielded.
|
87
|
+
+ #
|
88
|
+
+ # This should be overridden by subclasses, or used as a shortcut for your
|
89
|
+
+ # own plugin implementations
|
90
|
+
+ #
|
91
|
+
+ # This method is automatically called by the table renderer
|
92
|
+
+ #
|
93
|
+
def build_table_header
|
94
|
+
table_header_proc[pdf_writer] if table_header_proc
|
95
|
+
end
|
96
|
+
|
97
|
+
+ # Calls the draw_table method
|
98
|
+
+ #
|
99
|
+
+ # This method is automatically called by the table renderer
|
100
|
+
+ #
|
101
|
+
def build_table_body
|
102
|
+
draw_table
|
103
|
+
end
|
104
|
+
|
105
|
+
+ # If table_footer_proc is defined, it will be executed and the PDF::Writer
|
106
|
+
+ # object will be yielded.
|
107
|
+
+ #
|
108
|
+
+ # This should be overridden by subclasses, or used as a shortcut for your
|
109
|
+
+ # own plugin implementations
|
110
|
+
+ #
|
111
|
+
+ # This method is automatically called by the table renderer
|
112
|
+
+ #
|
113
|
+
def build_table_footer
|
114
|
+
table_footer_proc[pdf_writer] if table_footer_proc
|
115
|
+
end
|
116
|
+
|
117
|
+
+ # Appends the results of PDF::Writer#render to output for your
|
118
|
+
+ # <tt>pdf_writer</tt> object.
|
119
|
+
def finalize_table
|
120
|
+
output << pdf_writer.render
|
121
|
+
end
|
122
|
+
|
123
|
+
+ # Call PDF::Writer#text with the given arguments
|
124
|
+
def add_text(*args)
|
125
|
+
pdf_writer.text(*args)
|
126
|
+
end
|
127
|
+
|
128
|
+
+ # Adds n to PDF::Writer#y.
|
129
|
+
+ #
|
130
|
+
+ # Basically, this allows you to move the rendering cursor up and down the page.
|
131
|
+
+ #
|
132
|
+
+ # move_cursor(10) #=> move up 10
|
133
|
+
+ # move_cursor(-10) #=> move down 10
|
134
|
+
def move_cursor(n)
|
135
|
+
pdf_writer.y += n
|
136
|
+
end
|
137
|
+
|
138
|
+
+ # Sets PDF::Writer#y to n.
|
139
|
+
+ #
|
140
|
+
+ # This lets you move to a given height on the page.
|
141
|
+
+ #
|
142
|
+
def move_cursor_to(n)
|
143
|
+
pdf_writer.y = n
|
144
|
+
end
|
145
|
+
|
146
|
+
+ # creates a margin of <tt>y</tt> amount and allows a block to be called to
|
147
|
+
+ # render items within that margin.
|
148
|
+
+ #
|
149
|
+
+ # Example:
|
150
|
+
+ #
|
151
|
+
+ # pad(10) { add_text 'hello' } #=> creates text hello with vertical margin
|
152
|
+
+ # of 10.
|
153
|
+
def pad(y,&block)
|
154
|
+
move_cursor -y
|
155
|
+
block.call
|
156
|
+
move_cursor -y
|
157
|
+
end
|
158
|
+
|
159
|
+
+ # Builds a PDF::SimpleTable from a Data::Table
|
160
|
+
+ #
|
161
|
+
+ # Can be customized via the follow layout parameters:
|
162
|
+
+ #
|
163
|
+
+ # max_table_width #=> defaults to 500
|
164
|
+
+ # table_width #=> optionally set a fixed width for the table
|
165
|
+
+ # orientation #=> defaults to :center
|
166
|
+
def draw_table
|
167
|
+
m = "Sorry, cant build PDFs from array like things (yet)"
|
168
|
+
raise m if data.column_names.empty?
|