table_helper 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|