ar_to_html_table 0.0.1 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.textile +106 -0
- data/ar_to_html_table.gemspec +4 -3
- data/lib/ar_to_html_table/column_formats.rb +92 -0
- data/lib/ar_to_html_table/column_formatter.rb +197 -0
- data/lib/ar_to_html_table/model.rb +31 -0
- data/lib/ar_to_html_table/table_formatter.rb +286 -0
- data/lib/ar_to_html_table/version.rb +1 -1
- data/lib/ar_to_html_table.rb +11 -2
- data/lib/locale/cldr_additions.yml +12 -0
- data/lib/locale/en.yml +7 -0
- data/lib/tasks/html_tables_tasks.rake +4 -0
- metadata +29 -8
data/.gitignore
CHANGED
data/README.textile
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
h1. Description
|
2
|
+
|
3
|
+
p. ar_to_html_table renders an ActiveRecord result set into an html_table. For example:
|
4
|
+
|
5
|
+
bc. Product.all.to_table
|
6
|
+
|
7
|
+
p. will produce a table with default characteristics. These characteristics are primarily driven
|
8
|
+
by class names and hence styling is defined in CSS.
|
9
|
+
|
10
|
+
h1. Usage
|
11
|
+
|
12
|
+
p. ar_to_html_table consists of two parts:
|
13
|
+
* Defining column formats in the ActiveRecord model
|
14
|
+
* Rendering an ActiveRecord result set
|
15
|
+
|
16
|
+
h2. Column definitions
|
17
|
+
|
18
|
+
p. Columns in the table are formatted based upon a column definition applied in the Model. Some examples:
|
19
|
+
|
20
|
+
bc. class Product < ActiveRecord::Base
|
21
|
+
column_format :name, :order => 1
|
22
|
+
column_format :orders, :total => :sum
|
23
|
+
column_format :revenue, :total => :sum, :order => 5, :class => 'right'
|
24
|
+
column_format :age, :total => :avg, :order => 20, :class => 'right', :formatter => :number_with_delimiter
|
25
|
+
....
|
26
|
+
end
|
27
|
+
|
28
|
+
p. The general form of a column definition is:
|
29
|
+
|
30
|
+
bc. column_format :column_name, options
|
31
|
+
|
32
|
+
h2. Column names and calculated columns
|
33
|
+
|
34
|
+
p. Column names are generally the model attribute name however a limited set of calculated columns can also be derived. For example:
|
35
|
+
|
36
|
+
bc. column_format :percentage_of_revenue, :order => 9, :class => 'right'
|
37
|
+
|
38
|
+
p. Will define a column that renders the percentage of total revenue that this row's revenue represents. The regexp used to recognize calculated columns is:
|
39
|
+
|
40
|
+
bc. /(percent|percentage|difference|diff)_of_(.*)/
|
41
|
+
|
42
|
+
p. Where the match is the name of the column against which the calculation if made. Therefore percentage and difference (plus their variants) are the two available calculated column types.
|
43
|
+
|
44
|
+
h2. Column options
|
45
|
+
|
46
|
+
|_. Option|_. Description|
|
47
|
+
|:order|Positions a column order relative to other columns. The number isn't important, just its relative value compared to other columns. Columns are sorted by order and rendered in that order. The default order is the order in which the columns are defined.|
|
48
|
+
|:total|Renders a table footer with a calculation of all the values in the column. The available totaling methods are **:sum**, **:count** and **:average** (or :avg or :mean)|
|
49
|
+
|:class|The CSS class for this column. Note that a **colgroup** is defined for each column and each **colgroup** has as CSS class that is the column name|
|
50
|
+
|:formatter|Used to format the value of each table cell. There are several predefined formatters. This value can also be a lambda for arbitrary formatting.|
|
51
|
+
|
52
|
+
h2. Predefined formatters
|
53
|
+
|
54
|
+
|_. Formatter|_. Description|
|
55
|
+
|:float_with_precision|Calls **#number_with_precision** after **#to_f** on the value.|
|
56
|
+
|:integer_with_delimiter|Calls **#integer_with_delimiter** unless **I18n::Backend::Simple.included_modules.include? Cldr::Format** is true in which case **I18n.localize** is called.|
|
57
|
+
|:seconds_to_time|Formats an integer as hh:mm:ss, mostly used for durations, not time or datetime columns|
|
58
|
+
|:hours_to_time|Formats an integer as hh:00.|
|
59
|
+
|:currency_without_sign|Calls **#number_with_precision** with precision 2.|
|
60
|
+
|:percentage|An integer rendered as a percentage. Calls **#number_to_percentage** with a precision of 1.|
|
61
|
+
|:bar_and_percentage|Displays a CSS bar and a percentage.|
|
62
|
+
|:unknown_on_blank|Displays **(unknown)** when **column.blank?** is true. This is a localized value. The key **I18n.t('tables.unknown')** is used.|
|
63
|
+
|:not_set_on_blank|Displays **(not set)** when **column.blank?** is true. This is a localized value. The key **I18n.t('tables.not_set')** is used.|
|
64
|
+
|
65
|
+
p. If no formatter is specified then **#to_s** is called on the value unless the value is a **Fixnumn** in which case **#number_with_delimiter** is called.
|
66
|
+
|
67
|
+
h2. Table rendering
|
68
|
+
|
69
|
+
To render the html table, call **#to_table(options)** on any ActiveRecord result set. The default options are:
|
70
|
+
|
71
|
+
bc. :exclude => EXCLUDE_COLUMNS,
|
72
|
+
:exclude_ids => true,
|
73
|
+
:odd_row => "odd",
|
74
|
+
:even_row => "even",
|
75
|
+
:totals => true,
|
76
|
+
:total_one => 'tables.total_one',
|
77
|
+
:total_many => 'tables.total_many',
|
78
|
+
:unknown_key => 'tables.unknown',
|
79
|
+
:not_set_key => 'tables.not_set'
|
80
|
+
|
81
|
+
|_. Option|_. Description|
|
82
|
+
|:include|Array of columns that should be rendered|
|
83
|
+
|:exclude|Array of columns that should not be rendered|
|
84
|
+
|:exclude_ids|Don't render columns that end in **_id**|
|
85
|
+
|:sort|A **Proc** that is called to sort the rows. Called as **results.sort(options[:sort])**.|
|
86
|
+
|:heading|A table heading that is placed in the first row of a table|
|
87
|
+
|:caption|A table caption applied with <caption> markup|
|
88
|
+
|:odd_row|CSS class name for odd rows|
|
89
|
+
|:even_row|CSS class name for even rows|
|
90
|
+
|:totals|Add a total row at the bottom of the table|
|
91
|
+
|:total_one|I18n key for rendering the total row when the total is one|
|
92
|
+
|:total_many|I18n key for rendering the total row when the total is not one|
|
93
|
+
|:unknown_key|I18n key for rendering **(Unknown)**|
|
94
|
+
|:not_set_key|I18n key for rendering **(Not Set)**|
|
95
|
+
|
96
|
+
h1. License
|
97
|
+
|
98
|
+
(The MIT License)
|
99
|
+
|
100
|
+
Copyright © 2010 Kip Cole
|
101
|
+
|
102
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ‘Software’), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
103
|
+
|
104
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
105
|
+
|
106
|
+
THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/ar_to_html_table.gemspec
CHANGED
@@ -8,11 +8,11 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ["Kip Cole"]
|
10
10
|
s.email = ["kipcole9@gmail.com"]
|
11
|
-
s.homepage = "http://
|
11
|
+
s.homepage = "http://github.com/kipcole9/ar_to_html_table"
|
12
12
|
s.summary = %q{Render and ActiveRecord result set as an HTML table}
|
13
13
|
s.description = <<-EOF
|
14
|
-
Defines Array#to_table that will
|
15
|
-
|
14
|
+
Defines Array#to_table that will render an ActiveRecord result set
|
15
|
+
as an HTML table.
|
16
16
|
EOF
|
17
17
|
|
18
18
|
s.rubyforge_project = "ar_to_html_table"
|
@@ -21,4 +21,5 @@ Gem::Specification.new do |s|
|
|
21
21
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
22
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
23
|
s.require_paths = ["lib"]
|
24
|
+
s.add_dependency('builder')
|
24
25
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# Module included into ActiveRecord at gem activation. Includes methods
|
2
|
+
# to define column formats used when rendering an HTML table.
|
3
|
+
module ArToHtmlTable
|
4
|
+
module ColumnFormats
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
extend ClassMethods
|
8
|
+
include InstanceMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
# Define a column format.
|
18
|
+
#
|
19
|
+
# ====Options
|
20
|
+
#
|
21
|
+
# :order Defines the column output order relative to other columns
|
22
|
+
# :total Column totaling method
|
23
|
+
# :class CSS Class to be added to the table cell
|
24
|
+
# :formatter Formatter to be applied. Default is #to_s. A symbol or lambda. Symbol can
|
25
|
+
# represent any method that accepts a value and options including methods in
|
26
|
+
# ActionView::Helpers::NumberHelper
|
27
|
+
#
|
28
|
+
# See HtmlTable::ColumnFormatter for formatter options.
|
29
|
+
#
|
30
|
+
# ====Examples
|
31
|
+
#
|
32
|
+
# class Product < ActiveRecord::Base
|
33
|
+
# column_format :name, :order => 1
|
34
|
+
# column_format :orders, :total => :sum
|
35
|
+
# column_format :revenue, :total => :sum, :order => 5, :class => 'right'
|
36
|
+
# column_format :age, :total => :avg, :order => 20, :class => 'right', :formatter => :number_with_delimiter
|
37
|
+
# end
|
38
|
+
def column_format(method, options)
|
39
|
+
@attr_formats = (@attr_formats || default_formats).deep_merge({method.to_s => options})
|
40
|
+
end
|
41
|
+
alias :table_format :column_format
|
42
|
+
|
43
|
+
# Retrieve a column format.
|
44
|
+
#
|
45
|
+
# ====Examples
|
46
|
+
#
|
47
|
+
# # Given the following class definition
|
48
|
+
# class Product < ActiveRecord::Base
|
49
|
+
# column_format :name, :order => 1
|
50
|
+
# column_format :orders, :total => :sum
|
51
|
+
# column_format :revenue, :total => :sum, :order => 5, :class => 'right'
|
52
|
+
# column_format :age, :total => :avg, :order => 20, :class => 'right', :formatter => :number_with_delimiter
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# Product.format_of(:name)
|
56
|
+
# => { :order => 1 }
|
57
|
+
#
|
58
|
+
# Product.format_of(:revenue)
|
59
|
+
# => { :total => :sum, :order => 5, :class => 'right' }
|
60
|
+
def format_of(name)
|
61
|
+
@attr_formats ||= default_formats
|
62
|
+
@attr_formats[name] || {}
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
# Default column formats used in to_table for active_record
|
67
|
+
# result arrays
|
68
|
+
#
|
69
|
+
# Hash options are:
|
70
|
+
# => :class => 'class_name' # used to add a CSS class to the <td> element
|
71
|
+
# => :formatter => A symbol denoting a method or a proc to be used to
|
72
|
+
# format the data element. It will be passed the element only.
|
73
|
+
#
|
74
|
+
def default_formats
|
75
|
+
attr_formats = {}
|
76
|
+
columns.each do |column|
|
77
|
+
attr_formats[column.name] = case column.type
|
78
|
+
when :integer, :float
|
79
|
+
{:class => :right, :formatter => :number_with_delimiter}
|
80
|
+
when :text, :string
|
81
|
+
{}
|
82
|
+
when :date, :datetime
|
83
|
+
{}
|
84
|
+
else
|
85
|
+
{}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
attr_formats
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# ==Column Formatters
|
2
|
+
#
|
3
|
+
# Each cell value (attribute in a row) is formatted on output. This module
|
4
|
+
# defines a series of formatters. A formatter is any method with a signature
|
5
|
+
# of:
|
6
|
+
#
|
7
|
+
# <tt>def method(cell_value, options)</tt>
|
8
|
+
#
|
9
|
+
# Hence any method already in scope at the time of formatting is available as well.
|
10
|
+
# For example <tt>number_with_delimeter</tt> and friends are valid formatters.
|
11
|
+
module ArToHtmlTable
|
12
|
+
module ColumnFormatter
|
13
|
+
|
14
|
+
def self.included(base) #:nodoc:
|
15
|
+
#base.class_eval do
|
16
|
+
# extend ActiveSupport::Memoizable
|
17
|
+
# memoize :integer_with_delimiter
|
18
|
+
# memoize :float_with_precision
|
19
|
+
# memoize :currency_without_sign
|
20
|
+
#end
|
21
|
+
end
|
22
|
+
|
23
|
+
MIN_PERCENT_BAR_VALUE = 2.0 # Below which no bar is drawn
|
24
|
+
REDUCTION_FACTOR = 0.80 # Scale the bar graps so they have room for the percentage number in most cases
|
25
|
+
|
26
|
+
# If the value is #blank? then display a localized
|
27
|
+
# version of "Not Set".
|
28
|
+
#
|
29
|
+
# ====Examples
|
30
|
+
#
|
31
|
+
# # Given a value <em>nil</em> the following will be output
|
32
|
+
# # if the locale is set to "en" and the default translations
|
33
|
+
# # are not changed:
|
34
|
+
# (Not Set)
|
35
|
+
#
|
36
|
+
# val: the value to be formatted
|
37
|
+
# options: formatter options
|
38
|
+
def not_set_on_blank(val, options)
|
39
|
+
if options[:cell_type] == :th
|
40
|
+
val
|
41
|
+
else
|
42
|
+
val.blank? ? I18n.t(options[:not_set_key]) : val
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def group_not_set_on_blank(val, options) #:nodoc:
|
47
|
+
# Need more context to do this
|
48
|
+
end
|
49
|
+
|
50
|
+
# If the value is #blank? then display a localized
|
51
|
+
# version of "Unknown".
|
52
|
+
#
|
53
|
+
# ====Examples
|
54
|
+
#
|
55
|
+
# # Given a value _nil_ the following will be output
|
56
|
+
# # if the locale is set to "en" and the default translations
|
57
|
+
# # are not changed:
|
58
|
+
# (Unknown)
|
59
|
+
#
|
60
|
+
# val: the value to be formatted
|
61
|
+
# options: formatter options
|
62
|
+
def unknown_on_blank(val, options)
|
63
|
+
if options[:cell_type] == :th
|
64
|
+
val
|
65
|
+
else
|
66
|
+
val.blank? ? I18n.t(options[:unknown_key]) : val
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Interprets an integer as a duration and outputs the duration
|
71
|
+
# in the format hh:mm:ss
|
72
|
+
#
|
73
|
+
# ====Examples
|
74
|
+
#
|
75
|
+
# # Given a value of 3600, the formatter will output
|
76
|
+
# 00:05:00
|
77
|
+
#
|
78
|
+
# val: the value to be formatted
|
79
|
+
# options: formatter options
|
80
|
+
def seconds_to_time(val, options)
|
81
|
+
hours = val / 3600
|
82
|
+
minutes = (val / 60) - (hours * 60)
|
83
|
+
seconds = val % 60
|
84
|
+
(minutes += 1; seconds = 0) if seconds == 60
|
85
|
+
(hours += 1; minutes = 0) if minutes == 60
|
86
|
+
"#{"%02d" % hours}:#{"%02d" % minutes}:#{"%02d" % seconds}"
|
87
|
+
end
|
88
|
+
|
89
|
+
# Interprets an integer as a number of hours and outputs the value
|
90
|
+
# in the format hh:00
|
91
|
+
#
|
92
|
+
# ====Examples
|
93
|
+
#
|
94
|
+
# # Given a value of 11, the formatter will output
|
95
|
+
# 11:00
|
96
|
+
#
|
97
|
+
# val: the value to be formatted
|
98
|
+
# options: formatter options
|
99
|
+
def hours_to_time(val, options)
|
100
|
+
"#{"%02d" % val}:00"
|
101
|
+
end
|
102
|
+
|
103
|
+
# Interprets an integer as a percentage with a single
|
104
|
+
# digit of precision. Shim for <tt>#number_to_percentage</tt>
|
105
|
+
#
|
106
|
+
# ====Examples
|
107
|
+
#
|
108
|
+
# # Given a value of 48, the formatter will output
|
109
|
+
# 48.00%
|
110
|
+
#
|
111
|
+
# val: the value to be formatted
|
112
|
+
# options: formatter options
|
113
|
+
def percentage(val, options)
|
114
|
+
number_to_percentage(val ? val.to_f : 0, :precision => 1)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Formats a number as an integer with a delimiter. If Cldr::Format
|
118
|
+
# module is included into I18n then the value is localized (recommended
|
119
|
+
# for multilanguage applications). If not, number_with_delimiter is used
|
120
|
+
# formatting.
|
121
|
+
#
|
122
|
+
# ====Examples
|
123
|
+
#
|
124
|
+
# # Given a value of 1245 and no Cldr::Format, the formatter will output
|
125
|
+
# 1,345
|
126
|
+
#
|
127
|
+
# val: the value to be formatted
|
128
|
+
# options: formatter options
|
129
|
+
#
|
130
|
+
#--
|
131
|
+
# TODO this should be done just once at instantiation but we have a potential
|
132
|
+
# ordering issue since I18n initializer may not have run yet (needs to be checked)
|
133
|
+
def integer_with_delimiter(val, options = {})
|
134
|
+
if I18n::Backend::Simple.included_modules.include? Cldr::Format
|
135
|
+
I18n.localize(val.to_i, :format => :short)
|
136
|
+
else
|
137
|
+
number_with_delimiter(val.to_i)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Formats a number as an float with a delimiter and precision of 1.
|
142
|
+
#
|
143
|
+
# ====Examples
|
144
|
+
#
|
145
|
+
# # Given a value of 1245, the formatter will output
|
146
|
+
# 1,345.0
|
147
|
+
#
|
148
|
+
# val: the value to be formatted
|
149
|
+
# options: formatter options
|
150
|
+
def float_with_precision(val, options)
|
151
|
+
number_with_precision(val.to_f, :precision => 1)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Formats a number as an float with a delimiter and precision of 2.
|
155
|
+
#
|
156
|
+
# ====Examples
|
157
|
+
#
|
158
|
+
# # Given a value of 1245, the formatter will output
|
159
|
+
# 1,345.00
|
160
|
+
#
|
161
|
+
# val: the value to be formatted
|
162
|
+
# options: formatter options
|
163
|
+
def currency_without_sign(val, options)
|
164
|
+
number_with_precision(val.to_f, :precision => 2)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Formats a number as a horizontal CSS-based bar followed
|
168
|
+
# by the number formatted as a percentage.
|
169
|
+
#
|
170
|
+
# ====Examples
|
171
|
+
#
|
172
|
+
# # Given a value of 11, the formatter will output
|
173
|
+
# <tt><div class="hbar" style="width:#{width}%"> </div>
|
174
|
+
# <div>11%</div></tt>
|
175
|
+
#
|
176
|
+
# val: the value to be formatted
|
177
|
+
# options: formatter options
|
178
|
+
def bar_and_percentage(val, options)
|
179
|
+
if options[:cell_type] == :td
|
180
|
+
width = val * bar_reduction_factor(val)
|
181
|
+
bar = (val.to_f > MIN_PERCENT_BAR_VALUE) ? "<div class=\"hbar\" style=\"width:#{width}%\"> </div>" : ''
|
182
|
+
bar + "<div>" + percentage(val, :precision => 1) + "</div>"
|
183
|
+
else
|
184
|
+
percentage(val, :precision => 1)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
def bar_reduction_factor(value)
|
190
|
+
case value
|
191
|
+
when 0..79 then REDUCTION_FACTOR
|
192
|
+
when 80..99 then 0.6
|
193
|
+
else 0.3
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Adds method to Array to allow output of html tables - only works
|
2
|
+
# if the array is an ActiveRecord result set. See ArToHtmlTable::TableFormatter
|
3
|
+
module ArToHtmlTable
|
4
|
+
module Model
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
extend ClassMethods
|
8
|
+
include InstanceMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
# Renders an ActiveRecord result set into an HTML table
|
14
|
+
#
|
15
|
+
# ====Examples
|
16
|
+
#
|
17
|
+
# # Render all products as an HTML table
|
18
|
+
# Product.all.to_table
|
19
|
+
#
|
20
|
+
# See ArToHtmlTable::TableFormatter for options.
|
21
|
+
def to_table(options = {})
|
22
|
+
@formatter = ArToHtmlTable::TableFormatter.new(self, options)
|
23
|
+
@formatter.to_html
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,286 @@
|
|
1
|
+
module ArToHtmlTable
|
2
|
+
class TableFormatter
|
3
|
+
include ArToHtmlTable::ColumnFormatter
|
4
|
+
include ::ActionView::Helpers::NumberHelper
|
5
|
+
|
6
|
+
attr_accessor :html, :table_columns, :klass, :merged_options, :rows, :totals
|
7
|
+
attr_accessor :column_cache
|
8
|
+
|
9
|
+
EXCLUDE_COLUMNS = [:id, :updated_at, :created_at, :updated_on, :created_on]
|
10
|
+
CALCULATED_COLUMNS = /(percent|percentage|difference|diff)_of_(.*)/
|
11
|
+
DEFAULT_OPTIONS = {
|
12
|
+
:exclude => EXCLUDE_COLUMNS,
|
13
|
+
:exclude_ids => true,
|
14
|
+
:odd_row => "odd",
|
15
|
+
:even_row => "even",
|
16
|
+
:totals => true,
|
17
|
+
:total_one => 'tables.total_one',
|
18
|
+
:total_many => 'tables.total_many',
|
19
|
+
:unknown_key => 'tables.unknown',
|
20
|
+
:not_set_key => 'tables.not_set'
|
21
|
+
}
|
22
|
+
|
23
|
+
# Initialize a table formatter. Not normally called directly since
|
24
|
+
# Array#to_table takes care of this.
|
25
|
+
#
|
26
|
+
# results: the value to be formatted
|
27
|
+
# options: formatter options
|
28
|
+
#
|
29
|
+
# ====Options
|
30
|
+
#
|
31
|
+
# :include Array of attributes to include in the table. Default is all attributes excepted :excluded ones.
|
32
|
+
# :exclude Array of attributes to exclude from the table. Default is [:id, :updated_at, :created_at, :updated_on, :created_on]
|
33
|
+
# :exclude_ids Exclude attributes with names ending in '_id'. Default is _true_
|
34
|
+
# :sort A proc invoked to sort the rows before output. Default is not to sort.
|
35
|
+
# :heading Table heading places in the first row of a table
|
36
|
+
# :caption Table caption applied with <caption> markup
|
37
|
+
# :odd_row CSS Class name of the odd rows in the table. Default is _odd_
|
38
|
+
# :even_row CSS Class name of the even rows. Default is _even_
|
39
|
+
# :totals Include a total row if _true_. Default is _true_
|
40
|
+
# :total_one I18n key for displaying a table footer when there is one row. Default _tables.total_one_
|
41
|
+
# :total_many I18n key for displaying a table footer when there are > 1 rows. Default is _tables.total_many_
|
42
|
+
# :unknown_key I18n key for displaying _Unknown_. Default is _tables.unknown_
|
43
|
+
# :not_set_key I18n key for displaying _Not Set_. Default is _tables.no_set_
|
44
|
+
def initialize(results, options)
|
45
|
+
raise ArgumentError, "[to_table] First argument must be an array of ActiveRecord rows" \
|
46
|
+
unless results.try(:first).try(:class).try(:descends_from_active_record?) ||
|
47
|
+
results.is_a?(ActiveRecord::NamedScope::Scope)
|
48
|
+
|
49
|
+
raise ArgumentError, "[to_table] Sort option must be a Proc" \
|
50
|
+
if options[:sort] && !options[:sort].is_a?(Proc)
|
51
|
+
|
52
|
+
@klass = results.first.class
|
53
|
+
@rows = results
|
54
|
+
@column_order = 0
|
55
|
+
@merged_options = DEFAULT_OPTIONS.merge(options)
|
56
|
+
@table_columns = initialise_columns(rows, klass, merged_options)
|
57
|
+
@totals = initialise_totalling(rows, table_columns)
|
58
|
+
results.sort(options[:sort]) if options[:sort]
|
59
|
+
@merged_options[:rows] = results
|
60
|
+
@html = Builder::XmlMarkup.new(:indent => 2)
|
61
|
+
@column_cache = {}
|
62
|
+
end
|
63
|
+
|
64
|
+
# Render the result set to an HTML table using the
|
65
|
+
# options set at object instantiation.
|
66
|
+
#
|
67
|
+
# ====Examples
|
68
|
+
#
|
69
|
+
# products = Product.all
|
70
|
+
# formatter = ArToHtmlTable::TableFormatter.new(products)
|
71
|
+
# formatter.to_html
|
72
|
+
def to_html
|
73
|
+
options = merged_options
|
74
|
+
table_options = {}
|
75
|
+
html.table table_options do
|
76
|
+
html.caption(options[:caption]) if options[:caption]
|
77
|
+
output_table_headings(options)
|
78
|
+
output_table_footers(options)
|
79
|
+
html.tbody do
|
80
|
+
rows.each_with_index do |row, index|
|
81
|
+
output_row(row, index, options)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
protected
|
88
|
+
# Outputs colgroups and column headings
|
89
|
+
def output_table_headings(options)
|
90
|
+
# Table heading
|
91
|
+
html.colgroup do
|
92
|
+
table_columns.each {|column| html.col :class => column[:name] }
|
93
|
+
end
|
94
|
+
|
95
|
+
# Column groups
|
96
|
+
html.thead do
|
97
|
+
html.tr(options[:heading], :colspan => columns.length) if options[:heading]
|
98
|
+
html.tr do
|
99
|
+
table_columns.each do |column|
|
100
|
+
html_options = {}
|
101
|
+
html_options[:class] = column[:class] if column[:class]
|
102
|
+
html.th(column[:label], html_options)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Outputs one row
|
109
|
+
def output_row(row, count, options)
|
110
|
+
html_options = {}
|
111
|
+
html_options[:class] = (count.even? ? options[:even_row] : options[:odd_row])
|
112
|
+
html_options[:id] = row_id(row) if row[klass.primary_key]
|
113
|
+
html.tr html_options do
|
114
|
+
table_columns.each {|column| output_cell(row, column, options) }
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Outputs table footer
|
119
|
+
def output_table_footers(options)
|
120
|
+
output_table_totals(options) if options[:totals] && rows.length > 1
|
121
|
+
end
|
122
|
+
|
123
|
+
# Output totals row (calculations)
|
124
|
+
def output_table_totals(options)
|
125
|
+
return unless table_has_totals?
|
126
|
+
html.tfoot do
|
127
|
+
html.tr do
|
128
|
+
first_column = true
|
129
|
+
table_columns.each do |column|
|
130
|
+
value = first_column ? first_column_total(options) : totals[column[:name].to_s]
|
131
|
+
output_cell_value(:th, value, column, options)
|
132
|
+
first_column = false
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Outputs one cell
|
139
|
+
def output_cell(row, column, options = {})
|
140
|
+
output_cell_value(:td, row[column[:name]], column, options)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Outputs one cells value after invoking its formatter
|
144
|
+
def output_cell_value(cell_type, value, column, options = {})
|
145
|
+
column_name = column[:name].to_sym
|
146
|
+
column_cache[column_name] = {} unless column_cache.has_key?(column_name)
|
147
|
+
|
148
|
+
if column_cache[column_name].has_key?(value)
|
149
|
+
result = column_cache[column_name][value]
|
150
|
+
else
|
151
|
+
result = column[:formatter].call(value, options.reverse_merge({:cell_type => cell_type, :column => column}))
|
152
|
+
result ||= ''
|
153
|
+
column_cache[column_name][value] = result
|
154
|
+
end
|
155
|
+
html.__send__(cell_type, (column[:class] ? {:class => column[:class]} : {})) do
|
156
|
+
html << result
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
# Craft a CSS id
|
162
|
+
def row_id(row)
|
163
|
+
"#{klass.name.underscore}_#{row[klass.primary_key]}"
|
164
|
+
end
|
165
|
+
|
166
|
+
def default_formatter(data, options)
|
167
|
+
case data
|
168
|
+
when Fixnum
|
169
|
+
integer_with_delimiter(data, options)
|
170
|
+
else
|
171
|
+
data.to_s
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def table_has_totals?
|
176
|
+
!totals.empty?
|
177
|
+
end
|
178
|
+
|
179
|
+
def initialise_columns(rows, model, options)
|
180
|
+
options[:include] = options[:include].map(&:to_s) if options[:include]
|
181
|
+
options[:exclude] = options[:exclude].map(&:to_s) if options[:exclude]
|
182
|
+
add_calculated_columns_to_rows(rows, options)
|
183
|
+
requested_columns = columns_from_row(rows.first)
|
184
|
+
columns = requested_columns.inject([]) do |definitions, column|
|
185
|
+
definitions << column_definition(column) if include_column?(column, options)
|
186
|
+
definitions
|
187
|
+
end
|
188
|
+
columns.sort{|a, b| a[:order] <=> b[:order] }
|
189
|
+
end
|
190
|
+
|
191
|
+
# Return a hash of hashes
|
192
|
+
# :sum => {:column_name_1 => value, :column_name_2 => value}
|
193
|
+
def initialise_totalling(rows, columns)
|
194
|
+
columns.inject({}) do |totals, column|
|
195
|
+
case column[:total]
|
196
|
+
when :sum
|
197
|
+
totals[column[:name]] = rows.make_numeric(column[:name]).sum(column[:name])
|
198
|
+
when :mean, :average, :avg
|
199
|
+
totals[column[:name]] = rows.make_numeric(column[:name]).mean(column[:name])
|
200
|
+
when :count
|
201
|
+
totals[column[:name]] = rows.make_numeric(column[:name]).count(column[:name])
|
202
|
+
end
|
203
|
+
totals
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def first_column_total(options)
|
208
|
+
if rows.count > 1
|
209
|
+
I18n.t(options[:total_many], :count => rows.count)
|
210
|
+
else
|
211
|
+
I18n.t(options[:total_one], :count => rows.count)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def column_definition(column)
|
216
|
+
@column_order += 1
|
217
|
+
@default_formatter ||= procify(:default_formatter)
|
218
|
+
|
219
|
+
css_class, formatter = get_column_formatter(column.to_s)
|
220
|
+
column_order = klass.format_of(column)[:order] || @column_order
|
221
|
+
totals = klass.format_of(column)[:total]
|
222
|
+
return {
|
223
|
+
:name => column,
|
224
|
+
:label => klass.human_attribute_name(column),
|
225
|
+
:formatter => formatter || @default_formatter,
|
226
|
+
:class => css_class,
|
227
|
+
:order => column_order,
|
228
|
+
:total => totals
|
229
|
+
}
|
230
|
+
end
|
231
|
+
|
232
|
+
def columns_from_row(row)
|
233
|
+
row.attributes.inject([]) {|columns, (k, v)| columns << k.to_s }
|
234
|
+
end
|
235
|
+
|
236
|
+
def get_column_formatter(column)
|
237
|
+
format = klass.format_of(column)
|
238
|
+
case format
|
239
|
+
when Symbol
|
240
|
+
formatter = procify(format)
|
241
|
+
when Proc
|
242
|
+
formatter = format
|
243
|
+
when Hash
|
244
|
+
css_class = format[:class] if format[:class]
|
245
|
+
formatter = format[:formatter] if format[:formatter]
|
246
|
+
formatter = procify(formatter) if formatter && formatter.is_a?(Symbol)
|
247
|
+
end
|
248
|
+
return css_class, formatter
|
249
|
+
end
|
250
|
+
|
251
|
+
# A data formatter can be a symbol or a proc
|
252
|
+
# If its a symbol then we 'procify' it so that
|
253
|
+
# we have on calling interface in the output_cell method
|
254
|
+
# - partially for clarity and partially for performance
|
255
|
+
def procify(sym)
|
256
|
+
proc { |val, options| send(sym, val, options) }
|
257
|
+
end
|
258
|
+
|
259
|
+
# Decide if the given column is to be displayed in the table
|
260
|
+
def include_column?(column, options)
|
261
|
+
return options[:include].include?(column) if options[:include]
|
262
|
+
return false if options[:exclude] && options[:exclude].include?(column)
|
263
|
+
return false if options[:exclude_ids] && column.match(/_id\Z/)
|
264
|
+
true
|
265
|
+
end
|
266
|
+
|
267
|
+
def add_calculated_columns_to_rows(rows, options)
|
268
|
+
options.each do |k, v|
|
269
|
+
if match = k.to_s.match(CALCULATED_COLUMNS)
|
270
|
+
raise ArgumentError, "[to_table] Total value must not be 0 for percentage_of" if match[1] =~ /percent/ && v.to_f == 0
|
271
|
+
rows.each do |row|
|
272
|
+
row[k.to_s] = case match[1]
|
273
|
+
when 'percent', 'percentage'
|
274
|
+
row[match[2]].to_f / v.to_f * 100
|
275
|
+
when 'difference', 'diff'
|
276
|
+
row[match[2]].to_f - v.to_f
|
277
|
+
else
|
278
|
+
raise ArgumentError, "[to_table] Invalid calculated column '#{match[1]}' for '#{match[2]}'"
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
end
|
286
|
+
end
|
data/lib/ar_to_html_table.rb
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/ar_to_html_table/column_formatter.rb'
|
2
|
+
require File.dirname(__FILE__) + '/ar_to_html_table/table_formatter.rb'
|
3
|
+
require File.dirname(__FILE__) + '/ar_to_html_table/column_formats.rb'
|
4
|
+
require File.dirname(__FILE__) + '/ar_to_html_table/model.rb'
|
5
|
+
|
6
|
+
Array.send :include, ArToHtmlTable::Model
|
7
|
+
ActiveRecord::Base.send :include, ArToHtmlTable::ColumnFormats
|
8
|
+
I18n.load_path += Dir[ File.join(File.dirname(__FILE__), 'locale', '*.{rb,yml}') ]
|
9
|
+
|
1
10
|
module ArToHtmlTable
|
2
|
-
|
3
|
-
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# In table_formatter we user the decimal:patterns:short so we need to make sure it's there
|
2
|
+
# Some risk it is redefined by someone else too of course - but it's not part of the standard
|
3
|
+
# cldr database so should be OK.
|
4
|
+
en:
|
5
|
+
numbers:
|
6
|
+
formats:
|
7
|
+
decimal:
|
8
|
+
patterns:
|
9
|
+
short: "#,##0"
|
10
|
+
percent:
|
11
|
+
patterns:
|
12
|
+
full: "#,##0.###%"
|
data/lib/locale/en.yml
ADDED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ar_to_html_table
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 25
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
- 0
|
9
8
|
- 1
|
10
|
-
|
9
|
+
- 1
|
10
|
+
version: 0.1.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Kip Cole
|
@@ -15,11 +15,24 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-10-
|
18
|
+
date: 2010-10-17 00:00:00 +08:00
|
19
19
|
default_executable:
|
20
|
-
dependencies:
|
21
|
-
|
22
|
-
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: builder
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
description: " Defines Array#to_table that will render an ActiveRecord result set\n as an HTML table.\n"
|
23
36
|
email:
|
24
37
|
- kipcole9@gmail.com
|
25
38
|
executables: []
|
@@ -31,12 +44,20 @@ extra_rdoc_files: []
|
|
31
44
|
files:
|
32
45
|
- .gitignore
|
33
46
|
- Gemfile
|
47
|
+
- README.textile
|
34
48
|
- Rakefile
|
35
49
|
- ar_to_html_table.gemspec
|
36
50
|
- lib/ar_to_html_table.rb
|
51
|
+
- lib/ar_to_html_table/column_formats.rb
|
52
|
+
- lib/ar_to_html_table/column_formatter.rb
|
53
|
+
- lib/ar_to_html_table/model.rb
|
54
|
+
- lib/ar_to_html_table/table_formatter.rb
|
37
55
|
- lib/ar_to_html_table/version.rb
|
56
|
+
- lib/locale/cldr_additions.yml
|
57
|
+
- lib/locale/en.yml
|
58
|
+
- lib/tasks/html_tables_tasks.rake
|
38
59
|
has_rdoc: true
|
39
|
-
homepage: http://
|
60
|
+
homepage: http://github.com/kipcole9/ar_to_html_table
|
40
61
|
licenses: []
|
41
62
|
|
42
63
|
post_install_message:
|