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 CHANGED
@@ -1,3 +1,4 @@
1
1
  pkg/*
2
2
  *.gem
3
3
  .bundle
4
+ .DS_Store
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.
@@ -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://rubygems.org/gems/ar_to_html_table"
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 accept ActiveRecord result sets
15
- and render them as an html table.
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}%">&nbsp;</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}%\">&nbsp;</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
@@ -1,3 +1,3 @@
1
1
  module ArToHtmlTable
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -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
- # Your code goes here...
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
@@ -0,0 +1,7 @@
1
+ en:
2
+ tables:
3
+ total_many: "Total (%{count} rows)"
4
+ total_one: "Total (one row)"
5
+ not_set: (Not Set)
6
+ unknown: (Unknown)
7
+
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :html_tables do
3
+ # # Task goes here
4
+ # end
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: 29
4
+ hash: 25
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 0
9
8
  - 1
10
- version: 0.0.1
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-14 00:00:00 +08:00
18
+ date: 2010-10-17 00:00:00 +08:00
19
19
  default_executable:
20
- dependencies: []
21
-
22
- description: " Defines Array#to_table that will accept ActiveRecord result sets\n and render them as an html table.\n"
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://rubygems.org/gems/ar_to_html_table
60
+ homepage: http://github.com/kipcole9/ar_to_html_table
40
61
  licenses: []
41
62
 
42
63
  post_install_message: