report 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ 0.0.2 / 2012-07-04
2
+
3
+ * Bug fixes
4
+
5
+ * Properly handle true and false values in pdf and xlsx
6
+
7
+ * Enhancements
8
+
9
+ * More sensible default spacing, font size, and page margins for pdf output
10
+ * If a column is named "Did you really see it?", try #Did_you_really_see_it? and #did_you_really_see_it? (previously we lost ? and ! characters)
11
+ * If a "body.rows" method yields instead of just returning an enumerable, re-yield it
12
+ * If a column block takes 2 args (arity = 2), pass it report and row obj. This is useful when each row is calculated using (1) the row object and (2) something defined only on the report instance itself.
13
+
14
+ 0.0.1 / 2012-07-04
15
+
16
+ Initial release!
@@ -20,7 +20,9 @@ class Report
20
20
  attr_accessor :xlsx_format
21
21
 
22
22
  def table(table_name, &blk)
23
- tables << Table.new(table_name, &blk)
23
+ t = Table.new table_name
24
+ t.instance_eval(&blk)
25
+ tables << t
24
26
  end
25
27
 
26
28
  def format_pdf(hsh)
@@ -1,31 +1,43 @@
1
1
  require 'report/body/column'
2
2
  require 'report/body/row'
3
- require 'report/body/rows'
4
3
 
5
4
  class Report
6
5
  class Body
7
6
  attr_reader :table
8
7
  attr_reader :columns
9
- def initialize(table, &blk)
8
+ attr_reader :method_id
9
+ attr_reader :method_args
10
+ def initialize(table)
10
11
  @table = table
11
12
  @columns = []
12
- instance_eval(&blk)
13
13
  end
14
14
  def rows(*args)
15
- @rows = Rows.new(*([self]+args))
15
+ @method_id = args.shift
16
+ if args.last.is_a?(Array)
17
+ @method_args = args.last
18
+ end
16
19
  end
17
20
  def column(*args, &blk)
18
21
  @columns << Column.new(*([self]+args), &blk)
19
22
  end
20
23
  def each(report)
21
- @rows.each(report) do |obj|
22
- yield Row.new(self, obj)
24
+ block_taken = false
25
+ if method_args
26
+ enum = report.send(method_id, *method_args) do |obj|
27
+ block_taken = true
28
+ yield Row.new(self, report, obj)
29
+ end
30
+ else
31
+ enum = report.send(method_id) do |obj|
32
+ block_taken = true
33
+ yield Row.new(self, report, obj)
34
+ end
35
+ end
36
+ unless block_taken
37
+ enum.each do |obj|
38
+ yield Row.new(self, report, obj)
39
+ end
23
40
  end
24
- end
25
- def to_a(report)
26
- a = []
27
- each(report) { |row| a << row.to_a }
28
- a
29
41
  end
30
42
  end
31
43
  end
@@ -4,12 +4,12 @@ class Report
4
4
  attr_reader :body
5
5
  attr_reader :name
6
6
  attr_reader :method_id
7
- attr_reader :proc
7
+ attr_reader :blk
8
8
  attr_reader :faded
9
9
  attr_reader :row_options
10
- def initialize(*args, &proc)
10
+ def initialize(*args, &blk)
11
11
  if block_given?
12
- @proc = proc
12
+ @blk = blk
13
13
  end
14
14
  @body = args.shift
15
15
  @name = args.shift
@@ -18,9 +18,16 @@ class Report
18
18
  @faded = options.delete(:faded)
19
19
  @row_options = options
20
20
  end
21
- def read(obj)
22
- if @proc
23
- obj.instance_eval(&@proc)
21
+ def read(report, obj)
22
+ if blk
23
+ case blk.arity
24
+ when 0
25
+ obj.instance_eval(&blk)
26
+ when 2
27
+ blk.call report, obj
28
+ else
29
+ raise "column block should have 0 or 2 arguments"
30
+ end
24
31
  elsif method_id
25
32
  obj.send method_id
26
33
  elsif from_name = guesses.detect { |m| obj.respond_to?(m) }
@@ -29,8 +36,8 @@ class Report
29
36
  raise "#{obj.inspect} does not respond to any of #{guesses.inspect}"
30
37
  end
31
38
  end
32
- def read_with_options(obj)
33
- v = read obj
39
+ def read_with_options(report, obj)
40
+ v = read report, obj
34
41
  f = case faded
35
42
  when Symbol
36
43
  obj.send faded
@@ -41,7 +48,10 @@ class Report
41
48
  end
42
49
  private
43
50
  def guesses
44
- [ name, name.underscore.gsub(/\W/, '_') ]
51
+ [
52
+ name.gsub(' ', '_'),
53
+ name.underscore.gsub(/[^\w\!\?]/, '_')
54
+ ]
45
55
  end
46
56
  end
47
57
  end
@@ -3,16 +3,18 @@ class Report
3
3
  class Row
4
4
  attr_reader :body
5
5
  attr_reader :obj
6
- def initialize(body, obj)
6
+ attr_reader :report
7
+ def initialize(body, report, obj)
7
8
  @body = body
9
+ @report = report
8
10
  @obj = obj
9
11
  end
10
12
  def to_a
11
- body.columns.map { |column| column.read(obj) }
13
+ body.columns.map { |column| column.read(report, obj) }
12
14
  end
13
15
  def to_hash
14
16
  body.columns.map do |column|
15
- column.read_with_options obj
17
+ column.read_with_options report, obj
16
18
  end
17
19
  end
18
20
  end
@@ -1,25 +1,30 @@
1
- require 'report/head/row'
2
-
3
1
  class Report
4
2
  class Head
5
3
  attr_reader :table
6
- def initialize(table, &blk)
4
+ def initialize(table)
7
5
  @table = table
8
6
  @rows = []
9
- instance_eval(&blk)
10
7
  end
11
8
  def row(*cells)
12
- @rows << Row.new(self, cells)
9
+ @rows << cells
13
10
  end
14
11
  def each(report)
15
12
  @rows.each do |row|
16
- yield row.read(report)
13
+ actual = row.map do |cell|
14
+ case cell
15
+ when String
16
+ cell
17
+ when Symbol
18
+ unless report.respond_to?(cell)
19
+ raise "#{report.inspect} doesn't respond to #{cell.inspect}"
20
+ end
21
+ report.send cell
22
+ else
23
+ raise "must pass String or Symbol to head row"
24
+ end
25
+ end
26
+ yield actual
17
27
  end
18
28
  end
19
- def to_a(report)
20
- a = []
21
- each(report) { |row| a << row.to_a }
22
- a
23
- end
24
29
  end
25
30
  end
@@ -9,7 +9,7 @@ class Report
9
9
  :bold_italic => File.expand_path('../pdf/DejaVuSansMono-BoldOblique.ttf', __FILE__),
10
10
  }
11
11
  DEFAULT_DOCUMENT = {
12
- :top_margin => 118,
12
+ :top_margin => 72,
13
13
  :right_margin => 36,
14
14
  :bottom_margin => 72,
15
15
  :left_margin => 36,
@@ -40,19 +40,23 @@ class Report
40
40
  Prawn::Document.generate(tmp_path, document) do |pdf|
41
41
 
42
42
  pdf.font_families.update(font_name => font)
43
- pdf.font font_name
43
+ pdf.font font_name, :size => 10
44
44
 
45
+ first = true
45
46
  report.class.tables.each do |table|
46
- if table._head and (t = table._head.to_a(report)).length > 0
47
- pdf.table(t, head)
47
+ if first
48
+ first = false
49
+ else
50
+ pdf.move_down 20
51
+ end
52
+ if t = make(table._head)
53
+ pdf.table t, head
54
+ pdf.move_down 20
48
55
  end
49
-
50
- pdf.move_down 20
51
56
  pdf.text table.name, :style => :bold
52
- pdf.move_down 10
53
-
54
- if table._body and (t = table._body.to_a(report)).length > 0
55
- pdf.table(t, body)
57
+ if t = make(table._body)
58
+ pdf.move_down 10
59
+ pdf.table t, body
56
60
  end
57
61
  end
58
62
 
@@ -71,6 +75,23 @@ class Report
71
75
 
72
76
  private
73
77
 
78
+ def make(src)
79
+ return unless src
80
+ memo = []
81
+ src.each(report) do |row|
82
+ converted = row.to_a.map do |cell|
83
+ case cell
84
+ when TrueClass, FalseClass
85
+ cell.to_s
86
+ else
87
+ cell
88
+ end
89
+ end
90
+ memo << converted
91
+ end
92
+ memo if memo.length > 0
93
+ end
94
+
74
95
  def font_name
75
96
  'MainFont'
76
97
  end
@@ -1,15 +1,18 @@
1
1
  class Report
2
2
  class Table
3
3
  attr_reader :name
4
- def initialize(name, &blk)
4
+ def initialize(name)
5
5
  @name = name
6
- instance_eval(&blk)
7
6
  end
8
7
  def body(&blk)
9
- @body = Body.new(self, &blk)
8
+ b = Body.new self
9
+ b.instance_eval(&blk)
10
+ @body = b
10
11
  end
11
12
  def head(&blk)
12
- @head = Head.new(self, &blk)
13
+ h = Head.new self
14
+ h.instance_eval(&blk)
15
+ @head = h
13
16
  end
14
17
  def _head
15
18
  @head
@@ -1,3 +1,3 @@
1
1
  class Report
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -17,7 +17,7 @@ Gem::Specification.new do |gem|
17
17
  gem.version = Report::VERSION
18
18
 
19
19
  gem.add_runtime_dependency 'activesupport'
20
- gem.add_runtime_dependency 'xlsx_writer'
20
+ gem.add_runtime_dependency 'xlsx_writer', '>=0.2.2'
21
21
  gem.add_runtime_dependency 'prawn'
22
22
  gem.add_runtime_dependency 'posix-spawn'
23
23
 
@@ -6,14 +6,14 @@ require 'remote_table'
6
6
  require 'unix_utils'
7
7
  require 'posix/spawn'
8
8
 
9
- class Translation < Struct.new(:language, :translation)
9
+ class Translation < Struct.new(:language, :content)
10
10
  class << self
11
11
  def all
12
12
  [ new('English', 'Hello'), new('Russian', 'Здравствуйте') ]
13
13
  end
14
14
  end
15
15
  def backward
16
- translation.reverse
16
+ content.reverse
17
17
  end
18
18
  end
19
19
 
@@ -29,7 +29,7 @@ class A2 < Report
29
29
  body do
30
30
  rows :translations
31
31
  column 'Language'
32
- column 'Translation'
32
+ column 'Content'
33
33
  end
34
34
  end
35
35
  def translations
@@ -44,7 +44,7 @@ class A3 < Report
44
44
  body do
45
45
  rows :translations
46
46
  column 'Language'
47
- column 'Translation'
47
+ column 'Content'
48
48
  end
49
49
  end
50
50
  def description
@@ -59,7 +59,7 @@ class A4 < Report
59
59
  body do
60
60
  rows :translations
61
61
  column 'Language'
62
- column 'Forward', :translation
62
+ column 'Forward', :content
63
63
  column 'Backward'
64
64
  end
65
65
  end
@@ -72,14 +72,14 @@ class A5 < Report
72
72
  body do
73
73
  rows :translations, ['English']
74
74
  column 'Language'
75
- column 'Translation'
75
+ column 'Content'
76
76
  end
77
77
  end
78
78
  table 'InRussian' do
79
79
  body do
80
80
  rows :translations, ['Russian']
81
81
  column 'Language'
82
- column 'Translation'
82
+ column 'Content'
83
83
  end
84
84
  end
85
85
  def translations(language)
@@ -91,8 +91,8 @@ class A6 < Report
91
91
  body do
92
92
  rows :translations
93
93
  column 'Language'
94
- column('Forward') { translation }
95
- column('Backward') { translation.reverse }
94
+ column('Forward') { content }
95
+ column('Backward') { content.reverse }
96
96
  end
97
97
  end
98
98
  def translations
@@ -112,6 +112,33 @@ class A7 < Report
112
112
  end
113
113
  end
114
114
  end
115
+ class A8 < Report
116
+ table 'A8' do
117
+ body do
118
+ rows :translations
119
+ column 'Language'
120
+ column('ClassForward') { |report, translation| [report.class.name, translation.content].join }
121
+ column('ClassBackward') { |report, translation| [report.class.name.reverse, translation.content.reverse].join }
122
+ end
123
+ end
124
+ def translations
125
+ Translation.all
126
+ end
127
+ end
128
+ class A9 < Report
129
+ table 'MemorySafe' do
130
+ body do
131
+ rows :translations
132
+ column 'Language'
133
+ column 'Content'
134
+ end
135
+ end
136
+ def translations
137
+ Translation.all.each do |translation|
138
+ yield translation
139
+ end
140
+ end
141
+ end
115
142
  class Numero < Struct.new(:d_e_c_i_m_a_l, :m_o_n_e_y)
116
143
  class << self
117
144
  def all
@@ -159,6 +186,27 @@ class B2 < Report
159
186
  Numero.all
160
187
  end
161
188
  end
189
+ class Witness < Struct.new(:really_saw_it)
190
+ class << self
191
+ def all
192
+ [ new(true), new(false) ]
193
+ end
194
+ end
195
+ def did_you_really_see_it?
196
+ !!really_saw_it
197
+ end
198
+ end
199
+ class C1 < Report
200
+ table 'C1' do
201
+ body do
202
+ rows :witnesses
203
+ column 'Did you really see it?'
204
+ end
205
+ end
206
+ def witnesses
207
+ Witness.all
208
+ end
209
+ end
162
210
 
163
211
  describe Report do
164
212
  describe '#csv' do
@@ -169,9 +217,9 @@ describe Report do
169
217
  it "constructs a body out of rows and columns" do
170
218
  how_to_say_hello = ::CSV.read A2.new.csv.paths.first, :headers => :first_row
171
219
  how_to_say_hello[0]['Language'].should == 'English'
172
- how_to_say_hello[0]['Translation'].should == 'Hello'
220
+ how_to_say_hello[0]['Content'].should == 'Hello'
173
221
  how_to_say_hello[1]['Language'].should == 'Russian'
174
- how_to_say_hello[1]['Translation'].should == 'Здравствуйте'
222
+ how_to_say_hello[1]['Content'].should == 'Здравствуйте'
175
223
  end
176
224
  it "puts a blank row between head and body" do
177
225
  transl_with_head = ::CSV.read A3.new.csv.paths.first, :headers => false
@@ -196,13 +244,13 @@ describe Report do
196
244
  en = ::CSV.read en_path, :headers => :first_row
197
245
  en.length.should == 1
198
246
  en[0]['Language'].should == 'English'
199
- en[0]['Translation'].should == 'Hello'
247
+ en[0]['Content'].should == 'Hello'
200
248
  ru = ::CSV.read ru_path, :headers => :first_row
201
249
  ru.length.should == 1
202
250
  ru[0]['Language'].should == 'Russian'
203
- ru[0]['Translation'].should == 'Здравствуйте'
251
+ ru[0]['Content'].should == 'Здравствуйте'
204
252
  end
205
- it "instance-evals column blocks against row objects" do
253
+ it "instance-evals column blocks against row objects if arity is 0" do
206
254
  t = ::CSV.read A6.new.csv.paths.first, :headers => :first_row
207
255
  en = t[0]
208
256
  ru = t[1]
@@ -213,6 +261,29 @@ describe Report do
213
261
  ru['Forward'].should == 'Здравствуйте'
214
262
  ru['Backward'].should == 'Здравствуйте'.reverse
215
263
  end
264
+ it "calls column blocks with |report, row_obj| if arity == 2" do
265
+ t = ::CSV.read A8.new.csv.paths.first, :headers => :first_row
266
+ en = t[0]
267
+ ru = t[1]
268
+ en['Language'].should == 'English'
269
+ en['ClassForward'].should == 'A8Hello'
270
+ en['ClassBackward'].should == ['A8'.reverse, 'Hello'.reverse].join
271
+ ru['Language'].should == 'Russian'
272
+ ru['ClassForward'].should == 'A8Здравствуйте'
273
+ ru['ClassBackward'].should == ['A8'.reverse, 'Здравствуйте'.reverse].join
274
+ end
275
+ it "can have row objects yield to it one by one" do
276
+ how_to_say_hello = ::CSV.read A9.new.csv.paths.first, :headers => :first_row
277
+ how_to_say_hello[0]['Language'].should == 'English'
278
+ how_to_say_hello[0]['Content'].should == 'Hello'
279
+ how_to_say_hello[1]['Language'].should == 'Russian'
280
+ how_to_say_hello[1]['Content'].should == 'Здравствуйте'
281
+ end
282
+ it "treats ? and ! as valid method characters" do
283
+ t = ::CSV.read C1.new.csv.paths.first, :headers => :first_row
284
+ t[0]['Did you really see it?'].should == 'true'
285
+ t[1]['Did you really see it?'].should == 'false'
286
+ end
216
287
  end
217
288
 
218
289
  describe '#xlsx' do
@@ -223,9 +294,9 @@ describe Report do
223
294
  it "constructs a body out of rows and columns" do
224
295
  how_to_say_hello = RemoteTable.new A2.new.xlsx.path, :headers => :first_row
225
296
  how_to_say_hello[0]['Language'].should == 'English'
226
- how_to_say_hello[0]['Translation'].should == 'Hello'
297
+ how_to_say_hello[0]['Content'].should == 'Hello'
227
298
  how_to_say_hello[1]['Language'].should == 'Russian'
228
- how_to_say_hello[1]['Translation'].should == 'Здравствуйте'
299
+ how_to_say_hello[1]['Content'].should == 'Здравствуйте'
229
300
  end
230
301
  it "puts a blank row between head and body" do
231
302
  transl_with_head = RemoteTable.new A3.new.xlsx.path, :headers => false, :keep_blank_rows => true
@@ -250,11 +321,11 @@ describe Report do
250
321
  en = RemoteTable.new(path, :headers => :first_row, :sheet => 'InEnglish').to_a
251
322
  en.length.should == 1
252
323
  en[0]['Language'].should == 'English'
253
- en[0]['Translation'].should == 'Hello'
324
+ en[0]['Content'].should == 'Hello'
254
325
  ru = RemoteTable.new(path, :headers => :first_row, :sheet => 'InRussian').to_a
255
326
  ru.length.should == 1
256
327
  ru[0]['Language'].should == 'Russian'
257
- ru[0]['Translation'].should == 'Здравствуйте'
328
+ ru[0]['Content'].should == 'Здравствуйте'
258
329
  end
259
330
  it "instance-evals column blocks against row objects" do
260
331
  t = RemoteTable.new A6.new.xlsx.path, :headers => :first_row
@@ -267,6 +338,13 @@ describe Report do
267
338
  ru['Forward'].should == 'Здравствуйте'
268
339
  ru['Backward'].should == 'Здравствуйте'.reverse
269
340
  end
341
+ it "can have row objects yield to it one by one" do
342
+ how_to_say_hello = RemoteTable.new A9.new.xlsx.path, :headers => :first_row
343
+ how_to_say_hello[0]['Language'].should == 'English'
344
+ how_to_say_hello[0]['Content'].should == 'Hello'
345
+ how_to_say_hello[1]['Language'].should == 'Russian'
346
+ how_to_say_hello[1]['Content'].should == 'Здравствуйте'
347
+ end
270
348
  it "accepts a formatter that works on the raw XlsxWriter::Document" do
271
349
  path = A7.new.xlsx.path
272
350
  dir = UnixUtils.unzip path
@@ -290,6 +368,12 @@ describe Report do
290
368
  FileUtils.rm_f path
291
369
  FileUtils.rm_rf dir
292
370
  end
371
+ it "treats ? and ! as valid method characters" do
372
+ path = C1.new.xlsx.path
373
+ t = RemoteTable.new path, :headers => :first_row
374
+ t[0]['Did you really see it?'].should == 'TRUE'
375
+ t[1]['Did you really see it?'].should == 'FALSE'
376
+ end
293
377
  end
294
378
 
295
379
  describe '#pdf' do
@@ -350,11 +434,27 @@ describe Report do
350
434
  stdout_utf8.should include('Здравствуйте')
351
435
  stdout_utf8.should include('Здравствуйте'.reverse)
352
436
  end
437
+ it "can have row objects yield to it one by one" do
438
+ t = A9.new.pdf.path
439
+ child = POSIX::Spawn::Child.new('pdftotext', t, '-')
440
+ stdout_utf8 = child.out.force_encoding('UTF-8')
441
+ stdout_utf8.should include('English')
442
+ stdout_utf8.should include('Hello')
443
+ stdout_utf8.should include('Russian')
444
+ stdout_utf8.should include('Здравствуйте')
445
+ end
353
446
  it "accepts pdf formatting options, including the ability to stamp with pdftk" do
354
447
  path = B2.new.pdf.path
355
448
  child = POSIX::Spawn::Child.new('pdftotext', path, '-')
356
449
  stdout_utf8 = child.out.force_encoding('UTF-8')
357
450
  stdout_utf8.should include('Firefox')
358
451
  end
452
+ it "treats ? and ! as valid method characters" do
453
+ t = C1.new.pdf.path
454
+ child = POSIX::Spawn::Child.new('pdftotext', t, '-')
455
+ stdout_utf8 = child.out.force_encoding('UTF-8')
456
+ stdout_utf8.should include('true')
457
+ stdout_utf8.should include('false')
458
+ end
359
459
  end
360
460
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: report
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-07-04 00:00:00.000000000 Z
12
+ date: 2012-07-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -34,7 +34,7 @@ dependencies:
34
34
  requirements:
35
35
  - - ! '>='
36
36
  - !ruby/object:Gem::Version
37
- version: '0'
37
+ version: 0.2.2
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -42,7 +42,7 @@ dependencies:
42
42
  requirements:
43
43
  - - ! '>='
44
44
  - !ruby/object:Gem::Version
45
- version: '0'
45
+ version: 0.2.2
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: prawn
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -149,6 +149,7 @@ extra_rdoc_files: []
149
149
  files:
150
150
  - .gitignore
151
151
  - .yardopts
152
+ - CHANGELOG
152
153
  - Gemfile
153
154
  - LICENSE
154
155
  - README.md
@@ -158,13 +159,11 @@ files:
158
159
  - lib/report/body.rb
159
160
  - lib/report/body/column.rb
160
161
  - lib/report/body/row.rb
161
- - lib/report/body/rows.rb
162
162
  - lib/report/csv.rb
163
163
  - lib/report/csv/table.rb
164
164
  - lib/report/filename.rb
165
165
  - lib/report/formatter.rb
166
166
  - lib/report/head.rb
167
- - lib/report/head/row.rb
168
167
  - lib/report/pdf.rb
169
168
  - lib/report/pdf/DejaVuSansMono-Bold.ttf
170
169
  - lib/report/pdf/DejaVuSansMono-BoldOblique.ttf
@@ -1,21 +0,0 @@
1
- class Report
2
- class Body
3
- class Rows
4
- attr_reader :body
5
- attr_accessor :method_id
6
- attr_accessor :args
7
- def initialize(*args)
8
- @body = args.shift
9
- @method_id = args.shift
10
- if args.last.is_a?(Array)
11
- @args = args.last
12
- end
13
- end
14
- def each(report, &blk)
15
- (args ? report.send(method_id, *args) : report.send(method_id)).each do |obj|
16
- blk.call obj
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,27 +0,0 @@
1
- class Report
2
- class Head
3
- class Row
4
- attr_reader :head
5
- attr_reader :cells
6
- def initialize(head, cells)
7
- @head = head
8
- @cells = cells
9
- end
10
- def read(report)
11
- cells.map do |cell|
12
- case cell
13
- when String
14
- cell
15
- when Symbol
16
- unless report.respond_to?(cell)
17
- raise "#{report.inspect} doesn't respond to #{cell.inspect}"
18
- end
19
- report.send cell
20
- else
21
- raise "must pass String or Symbol to head row"
22
- end
23
- end
24
- end
25
- end
26
- end
27
- end