table_helper 0.0.3
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/CHANGELOG +19 -0
- data/MIT-LICENSE +20 -0
- data/README +161 -0
- data/Rakefile +80 -0
- data/init.rb +1 -0
- data/lib/table_helper/body.rb +119 -0
- data/lib/table_helper/body_row.rb +86 -0
- data/lib/table_helper/cell.rb +51 -0
- data/lib/table_helper/collection_table.rb +60 -0
- data/lib/table_helper/footer.rb +47 -0
- data/lib/table_helper/header.rb +125 -0
- data/lib/table_helper/html_element.rb +46 -0
- data/lib/table_helper/row.rb +72 -0
- data/lib/table_helper.rb +182 -0
- data/test/body_row_test.rb +105 -0
- data/test/body_test.rb +271 -0
- data/test/cell_test.rb +52 -0
- data/test/collection_table_test.rb +254 -0
- data/test/footer_test.rb +72 -0
- data/test/header_test.rb +210 -0
- data/test/html_element_test.rb +74 -0
- data/test/row_test.rb +115 -0
- data/test/table_helper_test.rb +37 -0
- data/test/test_helper.rb +14 -0
- metadata +85 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
*SVN*
|
2
|
+
|
3
|
+
*0.0.3* (June 1st, 2008)
|
4
|
+
|
5
|
+
* Remove dependency on set_or_append
|
6
|
+
|
7
|
+
*0.0.2* (May 5th, 2008)
|
8
|
+
|
9
|
+
* Updated documentation
|
10
|
+
|
11
|
+
*0.0.1* (August 18th, 2007)
|
12
|
+
|
13
|
+
* Add README documentation
|
14
|
+
|
15
|
+
* Add gem dependency on set_or_append
|
16
|
+
|
17
|
+
* Refactor test method names
|
18
|
+
|
19
|
+
* Convert dos newlines to unix newlines
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2006-2008 Aaron Pfeifer
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
= table_helper
|
2
|
+
|
3
|
+
+table_helper+ adds a helper method for generating HTML tables from collections.
|
4
|
+
|
5
|
+
== Resources
|
6
|
+
|
7
|
+
Wiki
|
8
|
+
|
9
|
+
* http://wiki.pluginaweek.org/Table_helper
|
10
|
+
|
11
|
+
API
|
12
|
+
|
13
|
+
* http://api.pluginaweek.org/table_helper
|
14
|
+
|
15
|
+
Development
|
16
|
+
|
17
|
+
* http://dev.pluginaweek.org/browser/trunk/table_helper
|
18
|
+
|
19
|
+
Source
|
20
|
+
|
21
|
+
* http://svn.pluginaweek.org/trunk/table_helper
|
22
|
+
|
23
|
+
== Description
|
24
|
+
|
25
|
+
Tables of summary data for ActiveRecord models are often formatted in the same
|
26
|
+
way by creating a header indicating the attribute and a body containing the
|
27
|
+
data from each record in separate rows. table_helper makes it easier to create
|
28
|
+
these types of tables by DRYing much of the html being generated.
|
29
|
+
|
30
|
+
== Usage
|
31
|
+
|
32
|
+
=== Basic Example
|
33
|
+
|
34
|
+
<%= collection_table Person.find(:all) %>
|
35
|
+
|
36
|
+
...is compiled to (formatted here for the sake of sanity):
|
37
|
+
|
38
|
+
<table cellpadding="0" cellspacing="0">
|
39
|
+
<thead>
|
40
|
+
<tr>
|
41
|
+
<th class="first_name" scope="col">First Name</th>
|
42
|
+
<th class="last_name" scope="col">Last Name</th>
|
43
|
+
<th class="company_id" scope="col">Company</th>
|
44
|
+
<th class="role" scope="col">Role</th>
|
45
|
+
</tr>
|
46
|
+
</thead>
|
47
|
+
<tbody>
|
48
|
+
<tr class="row">
|
49
|
+
<td class="first_name">John</td>
|
50
|
+
<td class="last_name">Doe</td>
|
51
|
+
<td class="company_id">1</td>
|
52
|
+
<td class="role">President</td>
|
53
|
+
</tr>
|
54
|
+
<tr class="row">
|
55
|
+
<td class="first_name">Jane</td>
|
56
|
+
<td class="last_name">Doe</td>
|
57
|
+
<td class="company_id">1</td>
|
58
|
+
<td class="role">Vice-President</td>
|
59
|
+
</tr>
|
60
|
+
</tbody>
|
61
|
+
<table>
|
62
|
+
|
63
|
+
=== Advanced Example
|
64
|
+
|
65
|
+
<%=
|
66
|
+
collection_table(@posts, {}, :id => 'posts', :class => 'summary') do |header, body|
|
67
|
+
header.column :title
|
68
|
+
header.column :category
|
69
|
+
header.column :author
|
70
|
+
header.column :publish_date, 'Date<br \>Published'
|
71
|
+
header.column :num_comments, '# Comments'
|
72
|
+
header.column :num_trackbacks, '# Trackbacks'
|
73
|
+
|
74
|
+
body.alternate = true
|
75
|
+
body.build do |row, post, index|
|
76
|
+
row.category post.category.name
|
77
|
+
row.author post.author.name
|
78
|
+
row.publish_date time_ago_in_words(post.published_on)
|
79
|
+
row.num_comments post.comments.empty? ? '-' : post.comments.size
|
80
|
+
row.num_trackbacks post.trackbacks.empty? ? '-' : post.trackbacks.size
|
81
|
+
end
|
82
|
+
end
|
83
|
+
%>
|
84
|
+
|
85
|
+
...is compiled to (formatted here for the sake of sanity):
|
86
|
+
|
87
|
+
<table cellpadding="0" cellspacing="0" class="summary" id="posts">
|
88
|
+
<thead>
|
89
|
+
<tr>
|
90
|
+
<th class="title" scope="col">Title</th>
|
91
|
+
<th class="category" scope="col">Category</th>
|
92
|
+
<th class="author" scope="col">Author</th>
|
93
|
+
<th class="publish_date" scope="col">Date<br \>Published</th>
|
94
|
+
<th class="num_comments" scope="col"># Comments</th>
|
95
|
+
<th class="num_trackbacks" scope="col"># Trackbacks</th>
|
96
|
+
</tr>
|
97
|
+
</thead>
|
98
|
+
<tbody class="alternate">
|
99
|
+
<tr class="row">
|
100
|
+
<td class="title">Open-source projects: The good, the bad, and the ugly</td>
|
101
|
+
<td class="category">General</td>
|
102
|
+
<td class="author">John Doe</td>
|
103
|
+
<td class="publish_date">23 days</td>
|
104
|
+
<td class="num_comments">-</td>
|
105
|
+
<td class="num_trackbacks">-</td>
|
106
|
+
</tr>
|
107
|
+
<tr class="row alternate">
|
108
|
+
<td class="title">5 reasons you should care about Rails</td>
|
109
|
+
<td class="category">Rails</td><td class="author">John Q. Public</td>
|
110
|
+
<td class="publish_date">21 days</td>
|
111
|
+
<td class="num_comments">-</td>
|
112
|
+
<td class="num_trackbacks">-</td>
|
113
|
+
</tr>
|
114
|
+
<tr class="row">
|
115
|
+
<td class="title">Deprecation: Stop digging yourself a hole</td>
|
116
|
+
<td class="category">Rails</td>
|
117
|
+
<td class="author">Jane Doe</td>
|
118
|
+
<td class="publish_date">17 days</td>
|
119
|
+
<td class="num_comments">-</td>
|
120
|
+
<td class="num_trackbacks">-</td>
|
121
|
+
</tr>
|
122
|
+
<tr class="row alternate">
|
123
|
+
<td class="title">Jumpstart your Rails career at RailsConf 2007</td>
|
124
|
+
<td class="category">Conferences</td>
|
125
|
+
<td class="author">Jane Doe</td>
|
126
|
+
<td class="publish_date">4 days</td>
|
127
|
+
<td class="num_comments">-</td>
|
128
|
+
<td class="num_trackbacks">-</td>
|
129
|
+
</tr>
|
130
|
+
<tr class="row">
|
131
|
+
<td class="title">Getting some REST</td>
|
132
|
+
<td class="category">Rails</td>
|
133
|
+
<td class="author">John Doe</td>
|
134
|
+
<td class="publish_date">about 18 hours</td>
|
135
|
+
<td class="num_comments">-</td>
|
136
|
+
<td class="num_trackbacks">-</td>
|
137
|
+
</tr>
|
138
|
+
</tbody>
|
139
|
+
</table>
|
140
|
+
|
141
|
+
=== Caveat Emptor
|
142
|
+
|
143
|
+
See the API for more information on syntax and options. You should only use
|
144
|
+
table_helper if it fits the needs of your application. Remember one of the key
|
145
|
+
principles of Rails, KISS (Keep It Simple Stupid). table_helper works really
|
146
|
+
well when you need to quickly output several of these types of summary tables.
|
147
|
+
If this is not the case, you may want to stick to using actual html.
|
148
|
+
|
149
|
+
== Testing
|
150
|
+
|
151
|
+
Before you can run any tests, the following gem must be installed:
|
152
|
+
* plugin_test_helper[http://wiki.pluginaweek.org/Plugin_test_helper]
|
153
|
+
|
154
|
+
To run against a specific version of Rails:
|
155
|
+
|
156
|
+
rake test RAILS_FRAMEWORK_ROOT=/path/to/rails
|
157
|
+
|
158
|
+
== Dependencies
|
159
|
+
|
160
|
+
* Rails 2.0 or later
|
161
|
+
* set_or_append[http://wiki.pluginaweek.org/Set_or_append]
|
data/Rakefile
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/contrib/sshpublisher'
|
5
|
+
|
6
|
+
PKG_NAME = 'table_helper'
|
7
|
+
PKG_VERSION = '0.0.3'
|
8
|
+
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
9
|
+
RUBY_FORGE_PROJECT = 'pluginaweek'
|
10
|
+
|
11
|
+
desc 'Default: run unit tests.'
|
12
|
+
task :default => :test
|
13
|
+
|
14
|
+
desc 'Test the table_helper plugin.'
|
15
|
+
Rake::TestTask.new(:test) do |t|
|
16
|
+
t.libs << 'lib'
|
17
|
+
t.pattern = 'test/**/*_test.rb'
|
18
|
+
t.verbose = true
|
19
|
+
end
|
20
|
+
|
21
|
+
desc 'Generate documentation for the table_helper plugin.'
|
22
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
23
|
+
rdoc.rdoc_dir = 'rdoc'
|
24
|
+
rdoc.title = 'TableHelper'
|
25
|
+
rdoc.template = '../rdoc_template.rb'
|
26
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
27
|
+
rdoc.rdoc_files.include('README')
|
28
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
29
|
+
end
|
30
|
+
|
31
|
+
spec = Gem::Specification.new do |s|
|
32
|
+
s.name = PKG_NAME
|
33
|
+
s.version = PKG_VERSION
|
34
|
+
s.platform = Gem::Platform::RUBY
|
35
|
+
s.summary = 'Adds a helper method for generating HTML tables from collections'
|
36
|
+
|
37
|
+
s.files = FileList['{lib,test}/**/*'].to_a + %w(CHANGELOG init.rb MIT-LICENSE Rakefile README)
|
38
|
+
s.require_path = 'lib'
|
39
|
+
s.autorequire = 'table_helper'
|
40
|
+
s.has_rdoc = true
|
41
|
+
s.test_files = Dir['test/**/*_test.rb']
|
42
|
+
|
43
|
+
s.author = 'Aaron Pfeifer'
|
44
|
+
s.email = 'aaron@pluginaweek.org'
|
45
|
+
s.homepage = 'http://www.pluginaweek.org'
|
46
|
+
end
|
47
|
+
|
48
|
+
Rake::GemPackageTask.new(spec) do |p|
|
49
|
+
p.gem_spec = spec
|
50
|
+
p.need_tar = true
|
51
|
+
p.need_zip = true
|
52
|
+
end
|
53
|
+
|
54
|
+
desc 'Publish the beta gem'
|
55
|
+
task :pgem => [:package] do
|
56
|
+
Rake::SshFilePublisher.new('aaron@pluginaweek.org', '/home/aaron/gems.pluginaweek.org/public/gems', 'pkg', "#{PKG_FILE_NAME}.gem").upload
|
57
|
+
end
|
58
|
+
|
59
|
+
desc 'Publish the API documentation'
|
60
|
+
task :pdoc => [:rdoc] do
|
61
|
+
Rake::SshDirPublisher.new('aaron@pluginaweek.org', "/home/aaron/api.pluginaweek.org/public/#{PKG_NAME}", 'rdoc').upload
|
62
|
+
end
|
63
|
+
|
64
|
+
desc 'Publish the API docs and gem'
|
65
|
+
task :publish => [:pgem, :pdoc, :release]
|
66
|
+
|
67
|
+
desc 'Publish the release files to RubyForge.'
|
68
|
+
task :release => [:gem, :package] do
|
69
|
+
require 'rubyforge'
|
70
|
+
|
71
|
+
ruby_forge = RubyForge.new.configure
|
72
|
+
ruby_forge.login
|
73
|
+
|
74
|
+
%w( gem tgz zip ).each do |ext|
|
75
|
+
file = "pkg/#{PKG_FILE_NAME}.#{ext}"
|
76
|
+
puts "Releasing #{File.basename(file)}..."
|
77
|
+
|
78
|
+
ruby_forge.add_release(RUBY_FORGE_PROJECT, PKG_NAME, PKG_VERSION, file)
|
79
|
+
end
|
80
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'table_helper'
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'table_helper/body_row'
|
2
|
+
|
3
|
+
module PluginAWeek #:nodoc:
|
4
|
+
module TableHelper
|
5
|
+
# Represents the body of the table. In HTML, you can think of this as
|
6
|
+
# the <tbody> tag of the table.
|
7
|
+
class Body < HtmlElement
|
8
|
+
# If set to :odd or :even, every odd or even-numbered row will have the
|
9
|
+
# class 'alternate' appended to its html attributes, respectively.
|
10
|
+
# Default is nil.
|
11
|
+
attr_accessor :alternate_rows
|
12
|
+
|
13
|
+
# The caption to display in the collection is empty
|
14
|
+
attr_accessor :empty_caption
|
15
|
+
|
16
|
+
def initialize(collection, header) #:nodoc:
|
17
|
+
super()
|
18
|
+
|
19
|
+
@collection, @header = collection, header
|
20
|
+
@empty_caption = 'No matches found.'
|
21
|
+
end
|
22
|
+
|
23
|
+
def alternate_rows=(value) #:nodoc:
|
24
|
+
raise ArgumentError, 'alternate_rows must be set to :odd or :even' if value && ![:odd, :even].include?(value)
|
25
|
+
@alternate_rows = value
|
26
|
+
end
|
27
|
+
|
28
|
+
# Builds the body of the table. This includes the actual data that is
|
29
|
+
# generated for each object in the collection.
|
30
|
+
#
|
31
|
+
# build expects a block that defines the data in each cell. Each
|
32
|
+
# iteration of the block will provide the object being rendered, the row
|
33
|
+
# within the table that will be built and the index of the object. For
|
34
|
+
# example,
|
35
|
+
#
|
36
|
+
# body.build do |row, post, index|
|
37
|
+
# row.title "<div class=\"wrapped\">#{post.title}</div>"
|
38
|
+
# row.category post.category.name
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# In addition, to specifying the data, you can also modify the html
|
42
|
+
# options of the row. For more information on doing this, see the
|
43
|
+
# BodyRow class.
|
44
|
+
#
|
45
|
+
# If the collection is empty and +empty_caption+ is set on the Body,
|
46
|
+
# then the actual body will be replaced by a single row containing the
|
47
|
+
# html that was stored in +empty_caption+.
|
48
|
+
#
|
49
|
+
# == Default Values
|
50
|
+
#
|
51
|
+
# Whenever possible, the default value of a cell will be set to the
|
52
|
+
# object's attribute with the same name as the cell. For example,
|
53
|
+
# if a Post consists of the attribute +title+, then the cell for the
|
54
|
+
# title will be prepopulated with that attribute's value:
|
55
|
+
#
|
56
|
+
# body.build do |row, post index|
|
57
|
+
# row.category post.category.name
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# +row.title+ is already set to post.category so there's no need to
|
61
|
+
# manually set the value of that cell. However, it is always possible
|
62
|
+
# to override the default value like so:
|
63
|
+
#
|
64
|
+
# body.build do |row, post, index|
|
65
|
+
# row.title link_to(post.title, post_url(post))
|
66
|
+
# row.category post.category.name
|
67
|
+
# end
|
68
|
+
def build(&block)
|
69
|
+
@content = ''
|
70
|
+
|
71
|
+
# Display nothing if there are no objects to display
|
72
|
+
if @collection.empty? && @empty_caption
|
73
|
+
row = Row.new
|
74
|
+
row[:class] = 'no_content'
|
75
|
+
|
76
|
+
html_options = {}
|
77
|
+
html_options[:colspan] = @header.column_names.size if @header.column_names.size > 1
|
78
|
+
row.cell nil, @empty_caption, html_options
|
79
|
+
|
80
|
+
@content << row.html
|
81
|
+
else
|
82
|
+
@collection.each_with_index do |object, i|
|
83
|
+
@content << build_row(object, i, &block)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
@content
|
88
|
+
end
|
89
|
+
|
90
|
+
# Builds a row for an object in the table.
|
91
|
+
#
|
92
|
+
# The provided block should set the values for each cell in the row.
|
93
|
+
def build_row(object, index = @collection.index(object), &block)
|
94
|
+
row = BodyRow.new(object, @header)
|
95
|
+
row.alternate = alternate_rows ? index.send("#{@alternate_rows}?") : false
|
96
|
+
|
97
|
+
yield row, object, index if block_given?
|
98
|
+
|
99
|
+
row.html
|
100
|
+
end
|
101
|
+
|
102
|
+
def html #:nodoc:
|
103
|
+
html_options = @html_options.dup
|
104
|
+
html_options[:class] = (html_options[:class].to_s + ' alternate').strip if alternate_rows
|
105
|
+
|
106
|
+
content_tag(tag_name, content, html_options)
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
def tag_name
|
111
|
+
'tbody'
|
112
|
+
end
|
113
|
+
|
114
|
+
def content
|
115
|
+
@content
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'table_helper/row'
|
2
|
+
|
3
|
+
module PluginAWeek #:nodoc:
|
4
|
+
module TableHelper
|
5
|
+
# Represents a single row within the body of a table. The row can consist
|
6
|
+
# of either data cells or header cells.
|
7
|
+
#
|
8
|
+
# == Borders
|
9
|
+
#
|
10
|
+
# Each row has an optional special border row that can be generated either
|
11
|
+
# immediately before or immediately after this row. A separate border row
|
12
|
+
# is usually used when you cannot express borders in the css of the row
|
13
|
+
# containing the data (e.g. dotted borders in Internet Explorer).
|
14
|
+
#
|
15
|
+
# To modify the properties of the border, you can access +row.border+ like
|
16
|
+
# so:
|
17
|
+
#
|
18
|
+
# r = BodyRow.new
|
19
|
+
# r.border_type = :before
|
20
|
+
# r.border[:style] = 'color: #ff0000;'
|
21
|
+
#
|
22
|
+
# == Alternating rows
|
23
|
+
#
|
24
|
+
# Alternating rows can be automated by setting the +alternate+ property.
|
25
|
+
# For example,
|
26
|
+
#
|
27
|
+
# r = BodyRow.new
|
28
|
+
# r.alternate = true
|
29
|
+
class BodyRow < Row
|
30
|
+
# True if this is an alternating row, otherwise false. Default is false.
|
31
|
+
attr_accessor :alternate
|
32
|
+
|
33
|
+
def initialize(object, header) #:nodoc:
|
34
|
+
super()
|
35
|
+
|
36
|
+
@header = header
|
37
|
+
@alternate = false
|
38
|
+
@html_options[:class] = ('row ' + @html_options[:class].to_s).strip
|
39
|
+
|
40
|
+
# For each column defined in the table, see if we can prepopulate the
|
41
|
+
# cell based on the data in the object. If not, we can at least
|
42
|
+
# provide shortcut accessors to the cell
|
43
|
+
@header.column_names.each do |column|
|
44
|
+
if object.respond_to?(column)
|
45
|
+
cell(column, object.send(column))
|
46
|
+
else
|
47
|
+
define_cell_accessor(column)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Generates the html for this row in additional to the border row
|
53
|
+
# (if specified)
|
54
|
+
def html
|
55
|
+
original_options = @html_options.dup
|
56
|
+
@html_options[:class] = (@html_options[:class].to_s + ' alternate').strip if alternate
|
57
|
+
html = super
|
58
|
+
@html_options = original_options
|
59
|
+
html
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
# Builds the row's cells based on the order of the columns in the
|
64
|
+
# header. If a cell cannot be found for a specific column, then a blank
|
65
|
+
# cell is rendered.
|
66
|
+
def content
|
67
|
+
number_to_skip = 0 # Keeps track of the # of columns to skip
|
68
|
+
|
69
|
+
html = ''
|
70
|
+
@header.column_names.each do |column|
|
71
|
+
number_to_skip -= 1 and next if number_to_skip > 0
|
72
|
+
|
73
|
+
if cell = @cells[column]
|
74
|
+
number_to_skip = (cell[:colspan] || 1) - 1
|
75
|
+
else
|
76
|
+
cell = Cell.new(column, '', :class => 'empty')
|
77
|
+
end
|
78
|
+
|
79
|
+
html << cell.html
|
80
|
+
end
|
81
|
+
|
82
|
+
html
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|