tabloid 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +7 -0
- data/README.txt +137 -0
- data/Rakefile +7 -0
- data/lib/tabloid.rb +18 -0
- data/lib/tabloid/column_extensions.rb +11 -0
- data/lib/tabloid/configuration.rb +20 -0
- data/lib/tabloid/data.rb +106 -0
- data/lib/tabloid/group.rb +69 -0
- data/lib/tabloid/header_row.rb +31 -0
- data/lib/tabloid/missing_element_error.rb +3 -0
- data/lib/tabloid/missing_parameter_error.rb +5 -0
- data/lib/tabloid/parameter.rb +15 -0
- data/lib/tabloid/report.rb +228 -0
- data/lib/tabloid/report_column.rb +31 -0
- data/lib/tabloid/row.rb +65 -0
- data/lib/tabloid/version.rb +3 -0
- data/spec/lib/tabloid/data_spec.rb +42 -0
- data/spec/lib/tabloid/group_spec.rb +72 -0
- data/spec/lib/tabloid/header_row_spec.rb +8 -0
- data/spec/lib/tabloid/report_spec.rb +157 -0
- data/spec/lib/tabloid/row_spec.rb +67 -0
- data/spec/spec_helper.rb +6 -0
- data/tabloid.gemspec +26 -0
- metadata +164 -0
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2011 Inductive Applications
|
2
|
+
|
3
|
+
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:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
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/README.txt
ADDED
@@ -0,0 +1,137 @@
|
|
1
|
+
Tabloid allows the creation of cacheable report data using a straightforward DSL and output to HTML, CSV, and more to come.
|
2
|
+
|
3
|
+
This gem comes out of an Austin.rb meeting about our favorite gems where I sketched out what my ideal reporting DSL would look like. This gem is inspired by some of the features of Ruport, but I found its API to be a bit top-heavy for rapidly producing reports (though at the time of this writing, Ruport is far more flexible than Tabloid) and its up to you to take care of items like data caching. If your reporting needs are fairly straightforward, Tabloid should work pretty well for your needs. That said, this gem is early in development and has a lot of rough edges (see TODO list below), but it is being used in production and makes me happy there.
|
4
|
+
|
5
|
+
Features:
|
6
|
+
* easy to use DSL for specifying report definitions
|
7
|
+
* built-in caching of compiled data to Memcached and Redis
|
8
|
+
* parameterized reports
|
9
|
+
* can be used with your choice of ORM (used in production with ActiveRecord, should be usable with any other ORM)
|
10
|
+
* grouping of data with group and report summaries (totals only at the moment)
|
11
|
+
* unicorns
|
12
|
+
|
13
|
+
How to use it
|
14
|
+
|
15
|
+
Simple report
|
16
|
+
class UnpaidInvoicesReport
|
17
|
+
include Tabloid::Report
|
18
|
+
|
19
|
+
element :invoice_number, "Invoice Number"
|
20
|
+
element :invoice_date, "Invoice Date"
|
21
|
+
element :customer_name, "Name"
|
22
|
+
element :invoice_amount, "Amount"
|
23
|
+
element :balance, "Balance"
|
24
|
+
|
25
|
+
rows do
|
26
|
+
Invoice.select(:invoice_number, :invoice_date, :customer_name, :invoice_amount, :balance).where("balance > 0")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
#create the report
|
31
|
+
report = UnpaidInvoicesReport.new
|
32
|
+
|
33
|
+
#collect the data
|
34
|
+
report.prepare
|
35
|
+
|
36
|
+
#output formats supported now
|
37
|
+
csv = report.to_csv
|
38
|
+
html = report.to_html
|
39
|
+
|
40
|
+
|
41
|
+
Walking through the above:
|
42
|
+
|
43
|
+
include Tabloid::Report
|
44
|
+
|
45
|
+
makes this class into a Tabloid report.
|
46
|
+
|
47
|
+
element :invoice_number, "Invoice Number"
|
48
|
+
|
49
|
+
#element creates a report column. In this case the report data will either use the first element of an array or whatever responds to the symbol :invoice_number on the data coming back from #rows. More on that in a sec...
|
50
|
+
|
51
|
+
rows do
|
52
|
+
Invoice.select(:invoice_number, :invoice_date, :customer_name, :invoice_amount, :balance).where("balance > 0")
|
53
|
+
end
|
54
|
+
|
55
|
+
#rows is the workhorse of a report. This is where you collect your data for reporting. It should return an array of arrays or an array of objects that respond to the keys dictated by your use of #element. (Support for an array of hashes is on the TODO list.) If you use nested arrays, the elements are order dependent—use the order you specified when adding element columns.
|
56
|
+
|
57
|
+
#to_csv and #to_html do pretty much what you'd think; they return a string containing the report in the respective formats. The HTML returned by #to_html is a table with one column per visible column; each cell will have the element symbol as a class name to allow for styling of columns.
|
58
|
+
|
59
|
+
Bells and whistles
|
60
|
+
Tabloid also supports groups with summaries and a report summary. Only totals are supported at the moment, but more flexibility is coming soon.
|
61
|
+
|
62
|
+
class UnpaidInvoicesReport < ActiveRecord::Base
|
63
|
+
include Tabloid::Report
|
64
|
+
handle_asynchronously :prepare
|
65
|
+
|
66
|
+
cache_key { "unpaid_invoices_report-#{id}"}
|
67
|
+
|
68
|
+
#parameterized report, supply parameters to report.prepare(...)
|
69
|
+
parameter :start_date
|
70
|
+
parameter :end_date
|
71
|
+
|
72
|
+
grouping :customer_name, :total => true
|
73
|
+
|
74
|
+
element :invoice_number, "Invoice Number"
|
75
|
+
element :invoice_date, "Invoice Date"
|
76
|
+
element :customer_name, "Name", :hidden => true
|
77
|
+
element :invoice_amount, "Amount", :total => true
|
78
|
+
element :balance, "Balance"
|
79
|
+
|
80
|
+
summary :balance => :sum
|
81
|
+
|
82
|
+
rows do
|
83
|
+
Invoice.select(:invoice_number, :invoice_date, :customer_name, :invoice_amount, :balance).where("balance > 0 AND invoice_date BETWEEN ? AND ?", parameter(:start_date), parameter(:end_date))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
There's several things different about this one. We use #grouping to tell Tabloid to group the data by customer name, and indicate that we want totals to be calculated for each group. We indicate which columns we want totalled by passing :total => true on the elements requiring totals. We tell Tabloid to hide the :customer_name column because it will show a group header that contains this element for us. Finally, we tell Tabloid to summarize the report by summing balances. (:sum is the only accepted value for now, but support is coming for arbitrary blocks and a wider range of built-in functions).
|
88
|
+
|
89
|
+
Background support
|
90
|
+
Notice the parent class on this one? This report is backed by ActiveRecord. The main reason you'd want to do that is to allow for generation of report data in the background. In the report above, we've enabled that by making #prepare (which invokes the #rows block) run in the background using DelayedJob's #handle_asynchronously method. To use it under these circumstances, you'll create and save the report, then call prepare explicitly:
|
91
|
+
|
92
|
+
report = UnpaidInvoicesReport.create
|
93
|
+
report.prepare(:start_date => 30.days.ago, :end_date => Date.today)
|
94
|
+
|
95
|
+
Caching support
|
96
|
+
Background generation of data wouldn't make sense to do, however, unless you were also caching the data somewhere. Tabloid has explicit support for caching using Memcached and Redis. Redis is preferred under most circumstances, as it doesn't have the 1MB record limit of memcached. To enable caching, you have to provide the #cache_key block above (see TODO for changes that are coming there) and set the caching parameters of Tabloid in an initializer:
|
97
|
+
|
98
|
+
#config/initializers/tabloid.rb
|
99
|
+
config = YAML.load(IO.read(Rails.root.join("config/tabloid.yml")))[Rails.env]
|
100
|
+
if config
|
101
|
+
Tabloid.cache_engine = config['cache_engine'].to_sym
|
102
|
+
Tabloid.cache_connection_options = {
|
103
|
+
:server => config['server'],
|
104
|
+
:port => config['port']
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
#config/tabloid.yml
|
109
|
+
production:
|
110
|
+
cache_engine: redis
|
111
|
+
server: 172.16.0.10
|
112
|
+
port: 6379
|
113
|
+
development:
|
114
|
+
cache_engine: memcached
|
115
|
+
server: localhost
|
116
|
+
port: 11211
|
117
|
+
test:
|
118
|
+
cache_engine: memcached
|
119
|
+
server: localhost
|
120
|
+
port: 11211
|
121
|
+
|
122
|
+
|
123
|
+
Caveat Emptor
|
124
|
+
This gem is being used in production without any real problems. There are definitely some rough edges to it; it was born out of the needs of a particular application, so while I've designed it to be something that isn't single purpose, its also geared towards basic reports that have totals in various configurations. So its a work in progress. That said, if you need to whip up a report quickly, give it a try. It has very few dependencies, as its being used with a Rails 2.3 application, so it doesn't pull in anything like ActiveSupport that would make it unsuitable for other environments.
|
125
|
+
|
126
|
+
Patches are welcome!
|
127
|
+
|
128
|
+
TODO:
|
129
|
+
* Add more options for summary rows, like average and arbitrary blocks
|
130
|
+
* clean up the test suite a bit
|
131
|
+
* documentation!
|
132
|
+
* more caching mechanisms
|
133
|
+
* better callbacks
|
134
|
+
* PDF output format support with PDFkit (optional)
|
135
|
+
* extend the summary method to support more complex summary formats
|
136
|
+
* Add support for a preamble section (e.g. detailing report parameters)
|
137
|
+
|
data/Rakefile
ADDED
data/lib/tabloid.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "builder"
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Tabloid
|
5
|
+
# Your code goes here...
|
6
|
+
end
|
7
|
+
require 'tabloid/configuration'
|
8
|
+
require 'tabloid/header_row'
|
9
|
+
require 'tabloid/missing_parameter_error'
|
10
|
+
require 'tabloid/missing_element_error'
|
11
|
+
require 'tabloid/parameter'
|
12
|
+
require 'tabloid/report'
|
13
|
+
require 'tabloid/column_extensions'
|
14
|
+
require 'tabloid/report_column'
|
15
|
+
require 'tabloid/row'
|
16
|
+
require 'tabloid/group'
|
17
|
+
require 'tabloid/data'
|
18
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Tabloid
|
2
|
+
def self.cache_engine=(engine)
|
3
|
+
@engine = engine
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.cache_engine
|
7
|
+
@engine
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.cache_enabled?
|
11
|
+
!@engine.nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.cache_connection_options=(options)
|
15
|
+
@cache_connection_options = options
|
16
|
+
end
|
17
|
+
def self.cache_connection_options
|
18
|
+
@cache_connection_options || {}
|
19
|
+
end
|
20
|
+
end
|
data/lib/tabloid/data.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
module Tabloid
|
2
|
+
class Data
|
3
|
+
attr_accessor :report_columns
|
4
|
+
attr_reader :rows
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
raise ArgumentError.new("Must supply row data") unless options[:rows]
|
8
|
+
raise ArgumentError.new("Must supply column data") unless options[:report_columns]
|
9
|
+
|
10
|
+
@report_columns = options[:report_columns]
|
11
|
+
@grouping_key = options[:grouping_key]
|
12
|
+
@grouping_options = options[:grouping_options] || {:total => true}
|
13
|
+
@summary_options = options[:summary] || {}
|
14
|
+
|
15
|
+
@rows = convert_rows(options[:rows])
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_csv
|
20
|
+
header_csv + rows.map(&:to_csv).join + summary_csv
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_html
|
24
|
+
header_html + rows.map(&:to_html).join + summary_html
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def convert_rows(rows)
|
29
|
+
rows.map! do |row|
|
30
|
+
Tabloid::Row.new(:columns => @report_columns, :data => row)
|
31
|
+
end
|
32
|
+
|
33
|
+
if @grouping_key
|
34
|
+
rows = rows.group_by { |r| r[@grouping_key] }
|
35
|
+
else
|
36
|
+
rows = {:default => rows}
|
37
|
+
end
|
38
|
+
|
39
|
+
rows.keys.sort.map do |key|
|
40
|
+
data_rows = rows[key]
|
41
|
+
|
42
|
+
label = (key == :default ? false : key)
|
43
|
+
Tabloid::Group.new :columns => @report_columns, :rows => data_rows, :label => label, :total => @grouping_options[:total]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def header_csv
|
48
|
+
FasterCSV.generate do |csv|
|
49
|
+
csv << @report_columns.map(&:to_header)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def header_html
|
54
|
+
headers = Builder::XmlMarkup.new
|
55
|
+
headers.tr do |tr|
|
56
|
+
@report_columns.each do |col|
|
57
|
+
tr.th(col.to_header, "class" => col.key) unless col.hidden?
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def summary_html
|
63
|
+
summary_rows.map { |row| row.to_html(:class => "summary") }.join
|
64
|
+
end
|
65
|
+
|
66
|
+
def summary_csv
|
67
|
+
summary_rows.map(&:to_csv).join
|
68
|
+
end
|
69
|
+
|
70
|
+
#perform the supplied block on all rows in the data structure
|
71
|
+
def summarize(key, block)
|
72
|
+
summaries = rows.map { |r| r.summarize(key, &block) }
|
73
|
+
if summaries.any?
|
74
|
+
summaries[1..-1].inject(summaries[0]) do |summary, val|
|
75
|
+
block.call(summary, val)
|
76
|
+
end
|
77
|
+
else
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def summary_rows
|
83
|
+
data_summary = report_columns.map do |col|
|
84
|
+
if summarizer = @summary_options[col.key]
|
85
|
+
summarize(col.key, self.send(summarizer)) unless col.hidden?
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
[
|
90
|
+
Tabloid::HeaderRow.new("Totals", :column_count => visible_column_count),
|
91
|
+
Tabloid::Row.new(:columns => @report_columns,
|
92
|
+
:data => data_summary)
|
93
|
+
|
94
|
+
]
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
def visible_column_count
|
99
|
+
@visible_col_count ||= @report_columns.select { |col| !col.hidden? }.count
|
100
|
+
end
|
101
|
+
|
102
|
+
def sum
|
103
|
+
proc(&:+)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
class Tabloid::Group
|
2
|
+
|
3
|
+
attr_reader :rows
|
4
|
+
attr_reader :columns
|
5
|
+
attr_reader :label
|
6
|
+
|
7
|
+
def initialize(options)
|
8
|
+
@rows = options[:rows]
|
9
|
+
@columns = options[:columns]
|
10
|
+
@visible_column_count = @columns.select { |col| !col.hidden? }.count
|
11
|
+
@total_required = options[:total]
|
12
|
+
@label = options[:label]
|
13
|
+
raise ArgumentError.new("Must supply row data to a Group") unless @rows
|
14
|
+
end
|
15
|
+
|
16
|
+
def total_required?
|
17
|
+
!(@total_required.nil? || @total_required == false)
|
18
|
+
end
|
19
|
+
|
20
|
+
def rows
|
21
|
+
if total_required?
|
22
|
+
summed_data = columns.map { |col| col.total? ? sum_rows(col.key) : nil }
|
23
|
+
@rows + [Tabloid::Row.new(:data => summed_data, :columns => self.columns)]
|
24
|
+
else
|
25
|
+
@rows
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def summarize(key, &block)
|
30
|
+
@rows[1..-1].inject(@rows[0].send(key)){|summary, row| block.call(summary, row.send(key)) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_csv
|
34
|
+
header_row_csv + rows.map(&:to_csv).join
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_html
|
38
|
+
header_row_html + rows.map(&:to_html).join
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def sum_rows(key)
|
43
|
+
#use the initial value from the same set of addends to prevent type conflict
|
44
|
+
#like 0:Fixnum + 0:Money => Exception
|
45
|
+
return nil unless @rows && @rows.any?
|
46
|
+
@rows[1..-1].inject(@rows[0][key]) { |sum, row| sum + row[key] }
|
47
|
+
end
|
48
|
+
|
49
|
+
def header_row_csv
|
50
|
+
if @label
|
51
|
+
cols = [label]
|
52
|
+
(@visible_column_count-1).times{ cols << nil}
|
53
|
+
FasterCSV.generate{|csv| csv<<cols}
|
54
|
+
else
|
55
|
+
""
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def header_row_html
|
60
|
+
if @label
|
61
|
+
html = Builder::XmlMarkup.new
|
62
|
+
html.tr(:class => "group_header") do |tr|
|
63
|
+
tr.td(label, {"colspan" => @visible_column_count})
|
64
|
+
end
|
65
|
+
else
|
66
|
+
""
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Tabloid::HeaderRow
|
2
|
+
def initialize(text, options={})
|
3
|
+
@text = text
|
4
|
+
@options = options
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_csv
|
8
|
+
FasterCSV.generate{|csv| csv << to_a}
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_html(options={})
|
12
|
+
html = Builder::XmlMarkup.new
|
13
|
+
html.tr("class" => (options[:class] || "header")) do |tr|
|
14
|
+
tr.td(@text, "colspan" => column_count)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_a
|
19
|
+
[@text].fill(nil, 1, column_count-1)
|
20
|
+
end
|
21
|
+
|
22
|
+
def summarize
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def column_count
|
28
|
+
(@options[:column_count] || 1)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Tabloid
|
2
|
+
class Parameter
|
3
|
+
attr_accessor :key, :label
|
4
|
+
def initialize(key, label = nil )
|
5
|
+
self.key = key
|
6
|
+
self.label = label || humanize(key.to_s)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def humanize(string)
|
11
|
+
"#{string.first.upcase}#{string[1..-1]}".gsub("_", " ")
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
module Tabloid::Report
|
2
|
+
|
3
|
+
def self.included(base)
|
4
|
+
base.class_eval do
|
5
|
+
@report_parameters = []
|
6
|
+
@report_columns = []
|
7
|
+
@report_columns.extend Tabloid::ColumnExtensions
|
8
|
+
extend Tabloid::Report::ClassMethods
|
9
|
+
include Tabloid::Report::InstanceMethods
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def parameter(*args)
|
15
|
+
@report_parameters << Tabloid::Parameter.new(*args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def store_parameters(attribute)
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
def parameters
|
23
|
+
@report_parameters
|
24
|
+
end
|
25
|
+
|
26
|
+
def summary(summary_options = {})
|
27
|
+
@summary_options = summary_options
|
28
|
+
end
|
29
|
+
|
30
|
+
def report_columns
|
31
|
+
@report_columns
|
32
|
+
end
|
33
|
+
|
34
|
+
def cache_key(&block)
|
35
|
+
if block
|
36
|
+
@cache_block = block
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def cache_key_block
|
41
|
+
@cache_block
|
42
|
+
end
|
43
|
+
|
44
|
+
def rows_block
|
45
|
+
@rows_block
|
46
|
+
end
|
47
|
+
|
48
|
+
def rows(*args, &block)
|
49
|
+
@rows_block = block
|
50
|
+
end
|
51
|
+
|
52
|
+
def element(key, label = "", options={})
|
53
|
+
@report_columns << Tabloid::ReportColumn.new(key, label, options)
|
54
|
+
end
|
55
|
+
|
56
|
+
def grouping(key, options = {})
|
57
|
+
@grouping_key = key
|
58
|
+
@grouping_options = options
|
59
|
+
end
|
60
|
+
|
61
|
+
def grouping_key
|
62
|
+
@grouping_key
|
63
|
+
end
|
64
|
+
|
65
|
+
def grouping_options
|
66
|
+
@grouping_options
|
67
|
+
end
|
68
|
+
|
69
|
+
def summary_options
|
70
|
+
@summary_options
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
module InstanceMethods
|
75
|
+
|
76
|
+
def prepare(options={})
|
77
|
+
before_prepare if self.respond_to?(:before_prepare)
|
78
|
+
@report_parameters = {}
|
79
|
+
parameters.each do |param|
|
80
|
+
value = options.delete param.key
|
81
|
+
raise Tabloid::MissingParameterError.new("Must supply :#{param.key} when creating the report") unless value
|
82
|
+
@report_parameters[param.key] = value
|
83
|
+
end
|
84
|
+
data
|
85
|
+
after_prepare if self.respond_to?(:after_prepare)
|
86
|
+
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
def report_columns
|
91
|
+
self.class.report_columns
|
92
|
+
end
|
93
|
+
|
94
|
+
def parameters
|
95
|
+
self.class.parameters
|
96
|
+
end
|
97
|
+
|
98
|
+
def parameter(key)
|
99
|
+
load_from_cache if Tabloid.cache_enabled?
|
100
|
+
@report_parameters[key] if @report_parameters
|
101
|
+
end
|
102
|
+
|
103
|
+
def data
|
104
|
+
load_from_cache if Tabloid.cache_enabled?
|
105
|
+
build_and_cache_data
|
106
|
+
@data
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_html
|
110
|
+
"<table>#{data.to_html}</table>"
|
111
|
+
end
|
112
|
+
|
113
|
+
def to_csv
|
114
|
+
data.to_csv
|
115
|
+
end
|
116
|
+
|
117
|
+
def cache_key
|
118
|
+
@key ||= begin
|
119
|
+
if self.class.cache_key_block
|
120
|
+
self.instance_exec &self.class.cache_key_block
|
121
|
+
else
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
private
|
129
|
+
def cache_data(data)
|
130
|
+
if Tabloid.cache_enabled?
|
131
|
+
raise Tabloid::MissingParameterError.new("Must supply a cache_key block when caching is enabled") unless self.class.cache_key_block
|
132
|
+
|
133
|
+
report_data = {
|
134
|
+
:parameters => @report_parameters,
|
135
|
+
:data => data
|
136
|
+
}
|
137
|
+
|
138
|
+
raise "Unable to cache data" unless cache_client.set(cache_key, YAML.dump(report_data))
|
139
|
+
|
140
|
+
end
|
141
|
+
data
|
142
|
+
end
|
143
|
+
|
144
|
+
def load_from_cache
|
145
|
+
if Tabloid.cache_enabled? && !@cached_data
|
146
|
+
@cached_data = read_from_cache
|
147
|
+
if @cached_data
|
148
|
+
@cached_data = YAML.load(@cached_data)
|
149
|
+
@data = @cached_data[:data]
|
150
|
+
@report_parameters = @cached_data[:parameters]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
def cache_client
|
157
|
+
if Tabloid.cache_enabled?
|
158
|
+
server = Tabloid.cache_connection_options[:server] || 'localhost'
|
159
|
+
if Tabloid.cache_engine == :memcached
|
160
|
+
port = Tabloid.cache_connection_options[:port] || '11211'
|
161
|
+
@cache_client ||= Dalli::Client.new("#{server}:#{port}")
|
162
|
+
elsif Tabloid.cache_engine == :redis
|
163
|
+
port = Tabloid.cache_connection_options[:port] || '6379'
|
164
|
+
@cache_client ||= Redis.new(
|
165
|
+
:host => server,
|
166
|
+
:port => port)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def build_and_cache_data
|
172
|
+
|
173
|
+
@data ||= begin
|
174
|
+
report_data = Tabloid::Data.new(
|
175
|
+
:report_columns => self.report_columns,
|
176
|
+
:rows => prepare_data,
|
177
|
+
:grouping_key => grouping_key,
|
178
|
+
:grouping_options => grouping_options,
|
179
|
+
:summary => summary_options
|
180
|
+
)
|
181
|
+
cache_data(report_data)
|
182
|
+
report_data
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
def prepare_data
|
188
|
+
row_data = instance_exec(&self.class.rows_block)
|
189
|
+
#unless row_data.first.is_a? Array
|
190
|
+
# row_data.map! do |row|
|
191
|
+
# report_columns.map do |col|
|
192
|
+
# row.send(col.key).to_s
|
193
|
+
# end
|
194
|
+
# end
|
195
|
+
#end
|
196
|
+
row_data
|
197
|
+
end
|
198
|
+
|
199
|
+
def read_from_cache
|
200
|
+
cache_client.get(cache_key) if cache_client && cache_key
|
201
|
+
end
|
202
|
+
|
203
|
+
def grouping_options
|
204
|
+
self.class.grouping_options
|
205
|
+
end
|
206
|
+
|
207
|
+
def grouping_key
|
208
|
+
self.class.grouping_key
|
209
|
+
end
|
210
|
+
|
211
|
+
def summary_options
|
212
|
+
self.class.summary_options
|
213
|
+
end
|
214
|
+
|
215
|
+
def parameter_info_html
|
216
|
+
html = Builder::XmlMarkup.new
|
217
|
+
html = html.p("id" => "parameters") do |p|
|
218
|
+
parameters.each do |param|
|
219
|
+
p.div do |div|
|
220
|
+
div.span(param.label, "class" => "parameter_label")
|
221
|
+
div.span(parameter(param.key), "class" => "parameter_value")
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
html.to_s
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Tabloid
|
2
|
+
class ReportColumn
|
3
|
+
attr_accessor :key
|
4
|
+
attr_accessor :label
|
5
|
+
attr_accessor :hidden
|
6
|
+
|
7
|
+
def initialize(key, label = "", options={})
|
8
|
+
self.key = key
|
9
|
+
self.label = label
|
10
|
+
@hidden = options[:hidden]
|
11
|
+
@total = options[:total]
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
@key.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def total?
|
19
|
+
@total
|
20
|
+
end
|
21
|
+
|
22
|
+
def hidden?
|
23
|
+
hidden
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_header
|
27
|
+
return self.label if label
|
28
|
+
return self.key
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/tabloid/row.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
class Tabloid::Row
|
2
|
+
def initialize(*args)
|
3
|
+
options = args.pop
|
4
|
+
if args.first
|
5
|
+
@data = args.first
|
6
|
+
else
|
7
|
+
@data = options[:data]
|
8
|
+
end
|
9
|
+
raise "Must supply data to .new when creating a new Row" unless @data
|
10
|
+
|
11
|
+
@columns = options[:columns]
|
12
|
+
raise "Must supply column information when creating a new Row" unless @columns
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def to_csv
|
17
|
+
FasterCSV.generate do |csv|
|
18
|
+
csv_array = []
|
19
|
+
@columns.each_with_index do |col, index|
|
20
|
+
next if col.hidden?
|
21
|
+
val = self[col.key]
|
22
|
+
csv_array << val
|
23
|
+
end
|
24
|
+
csv << csv_array
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_html(options = {})
|
29
|
+
html = Builder::XmlMarkup.new
|
30
|
+
html.tr("class" => (options[:class] || "data")) do |tr|
|
31
|
+
@columns.each_with_index do |col, index|
|
32
|
+
tr.td(self[col.key], "class" => col.key) unless col.hidden?
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def summarize(key, &block)
|
38
|
+
self[key]
|
39
|
+
end
|
40
|
+
|
41
|
+
def [](key)
|
42
|
+
if @data.is_a? Array
|
43
|
+
if key.is_a? Numeric
|
44
|
+
@data[key]
|
45
|
+
else
|
46
|
+
index = @columns.index{|col| col.key == key}
|
47
|
+
@data[index]
|
48
|
+
end
|
49
|
+
else
|
50
|
+
if key.is_a? Numeric
|
51
|
+
key = @columns[key].key
|
52
|
+
end
|
53
|
+
@data.send(key)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def method_missing(method, *args)
|
58
|
+
if @columns.detect{|col| col.key == method}
|
59
|
+
self[method]
|
60
|
+
else
|
61
|
+
super
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Tabloid::Data do
|
4
|
+
let(:columns) do
|
5
|
+
[
|
6
|
+
Tabloid::ReportColumn.new(:col1, "Column 1"),
|
7
|
+
Tabloid::ReportColumn.new(:col2, "Column 2")
|
8
|
+
]
|
9
|
+
end
|
10
|
+
let(:rows) { [[1, 2], [3, 4]] }
|
11
|
+
describe "creation" do
|
12
|
+
it "works when rows and columns are provided" do
|
13
|
+
lambda { Tabloid::Data.new(:report_columns => columns, :rows => rows) }.should_not raise_error(ArgumentError)
|
14
|
+
|
15
|
+
end
|
16
|
+
it "requires row data" do
|
17
|
+
lambda { Tabloid::Data.new(:report_columns => columns) }.should raise_error(ArgumentError, "Must supply row data")
|
18
|
+
end
|
19
|
+
it "requires column data" do
|
20
|
+
lambda { Tabloid::Data.new(:rows => rows) }.should raise_error(ArgumentError, "Must supply column data")
|
21
|
+
end
|
22
|
+
|
23
|
+
it "puts rows into groups" do
|
24
|
+
data = Tabloid::Data.new(:report_columns => columns, :rows => rows, :grouping_key => :col1)
|
25
|
+
data.rows.first.should be_a(Tabloid::Group)
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "summary" do
|
29
|
+
let(:data){ Tabloid::Data.new(:report_columns => columns, :rows => rows, :summary => { :col2 => :sum } )}
|
30
|
+
it "adds a totals row to the csv output" do
|
31
|
+
csv_rows = FasterCSV.parse(data.to_csv)
|
32
|
+
csv_rows.should include(["Totals", nil])
|
33
|
+
csv_rows.should include([nil, "6"])
|
34
|
+
end
|
35
|
+
it "adds a totals row to the html output" do
|
36
|
+
doc = Nokogiri::HTML(data.to_html)
|
37
|
+
(doc/"tr.summary").should_not be_nil
|
38
|
+
(doc/"tr.summary td.col2").text.should == "6"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Tabloid::Group do
|
4
|
+
let(:columns) do
|
5
|
+
[
|
6
|
+
Tabloid::ReportColumn.new(:col1, "Column 1"),
|
7
|
+
Tabloid::ReportColumn.new(:col2, "Column 2")
|
8
|
+
]
|
9
|
+
end
|
10
|
+
let(:row1) { Tabloid::Row.new(:columns => columns, :data => [1, 2]) }
|
11
|
+
let(:row2) { Tabloid::Row.new(:columns => columns, :data => [3, 4]) }
|
12
|
+
let(:group) { Tabloid::Group.new(:rows =>[row1, row2], :columns => columns, :label => "foobar") }
|
13
|
+
let(:anon_group) { Tabloid::Group.new(:rows =>[row1, row2], :columns => columns, :label => false) }
|
14
|
+
|
15
|
+
it "has a label" do
|
16
|
+
group.label.should == "foobar"
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "producing output" do
|
20
|
+
describe "as CSV" do
|
21
|
+
let(:rows) { FasterCSV.parse(group.to_csv) }
|
22
|
+
it "includes all rows for the group" do
|
23
|
+
rows.should include(["1", "2"])
|
24
|
+
rows.should include(["3", "4"])
|
25
|
+
end
|
26
|
+
it "includes a group label row" do
|
27
|
+
rows.should include(["foobar", nil])
|
28
|
+
end
|
29
|
+
it "doesn't include a label row when label is falsey" do
|
30
|
+
rows = FasterCSV.parse(anon_group.to_csv)
|
31
|
+
rows.should_not include(["foobar", nil])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
describe "as html" do
|
35
|
+
let(:doc) { doc = Nokogiri::HTML(group.to_html)
|
36
|
+
}
|
37
|
+
it "creates a table row for each data row" do
|
38
|
+
(doc/"tr[class='data']").count.should == 2
|
39
|
+
end
|
40
|
+
|
41
|
+
it "includes a group label row" do
|
42
|
+
(doc/"tr[class = 'group_header']")[0].text.should == "foobar"
|
43
|
+
end
|
44
|
+
it "doesn't include a label row when label is false" do
|
45
|
+
doc = Nokogiri::HTML(anon_group.to_html)
|
46
|
+
(doc/"tr[class='group_header']").count.should == 0
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#summarize" do
|
51
|
+
it "performs the supplied operation on the indicated column" do
|
52
|
+
group.summarize(:col1, &:+).should == 4
|
53
|
+
group.summarize(:col2, &:+).should == 6
|
54
|
+
end
|
55
|
+
end
|
56
|
+
context "with totals enabled" do
|
57
|
+
describe "#rows" do
|
58
|
+
it "includes a total row" do
|
59
|
+
columns = [
|
60
|
+
Tabloid::ReportColumn.new(:col1, "Column 1", :total => true),
|
61
|
+
Tabloid::ReportColumn.new(:col2, "Column 2", :total => true)
|
62
|
+
]
|
63
|
+
total_group = Tabloid::Group.new(:rows =>[row1, row2], :columns => columns, :total => true)
|
64
|
+
rows = total_group.rows
|
65
|
+
rows.count.should == 3
|
66
|
+
rows.last[:col1].should == 4
|
67
|
+
rows.last[:col2].should == 6
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Tabloid::Report do
|
4
|
+
|
5
|
+
context "producing output" do
|
6
|
+
class CsvReport
|
7
|
+
DATA=[
|
8
|
+
[1, 2],
|
9
|
+
[3, 4]
|
10
|
+
]
|
11
|
+
include Tabloid::Report
|
12
|
+
|
13
|
+
parameter :param1, "TestParameter"
|
14
|
+
|
15
|
+
element :col1, 'Col1'
|
16
|
+
element :col2, 'Col2'
|
17
|
+
|
18
|
+
cache_key{'report'}
|
19
|
+
|
20
|
+
rows do
|
21
|
+
CsvReport::DATA
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
before do
|
26
|
+
@report = CsvReport.new
|
27
|
+
@report.prepare(:param1 => "foobar")
|
28
|
+
end
|
29
|
+
|
30
|
+
context "with memcached caching" do
|
31
|
+
before do
|
32
|
+
Tabloid.cache_engine = :memcached
|
33
|
+
Tabloid.cache_connection_options = {
|
34
|
+
:server => "localhost",
|
35
|
+
:port => "11211"
|
36
|
+
}
|
37
|
+
end
|
38
|
+
after do
|
39
|
+
dc = Dalli::Client.new
|
40
|
+
dc.set("report", nil)
|
41
|
+
Tabloid.cache_engine = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#data" do
|
45
|
+
it "should cache after collecting the data" do
|
46
|
+
Dalli::Client.any_instance.stub(:get).and_return(nil)
|
47
|
+
Dalli::Client.any_instance.should_receive(:set).with('report', anything).and_return(true)
|
48
|
+
@report.data
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should return the cached data if it exists" do
|
52
|
+
Dalli::Client.any_instance.stub(:get).with('report').and_return(YAML.dump(@report.data))
|
53
|
+
Dalli::Client.any_instance.stub(:set).and_return(true)
|
54
|
+
|
55
|
+
@report.data.rows.should_not be_nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "#to_csv" do
|
61
|
+
it "includes headers by default" do
|
62
|
+
csv_output = FasterCSV.parse(@report.to_csv)
|
63
|
+
headers = csv_output.first
|
64
|
+
headers.first.should match(/Col1/)
|
65
|
+
headers.last.should match(/Col2/)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "includes the data from the report" do
|
69
|
+
csv_output = FasterCSV.parse(@report.to_csv)
|
70
|
+
csv_output.should include( ['1', '2'])
|
71
|
+
csv_output.should include( ['3', '4'])
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#to_html" do
|
76
|
+
let(:doc){Nokogiri::HTML(@report.to_html)}
|
77
|
+
it "creates a table" do
|
78
|
+
(doc/"table").count.should == 1
|
79
|
+
end
|
80
|
+
|
81
|
+
it "includes parameter information" do
|
82
|
+
pending("Need to put parameter block in the report")
|
83
|
+
(doc/".parameter_label").text.should include("TestParameter")
|
84
|
+
(doc/".parameter_value").text.should include("foobar")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
describe "#element" do
|
91
|
+
class ElementTestReport
|
92
|
+
include Tabloid::Report
|
93
|
+
element :col1
|
94
|
+
rows do
|
95
|
+
[[1,2]]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
before do
|
100
|
+
@report = ElementTestReport.new
|
101
|
+
end
|
102
|
+
|
103
|
+
it "adds a column to the report data" do
|
104
|
+
@report.data.report_columns[:col1].should_not be_nil
|
105
|
+
@report.data.report_columns[0].key.to_s.should == "col1"
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "grouping" do
|
111
|
+
class GroupingTest
|
112
|
+
include Tabloid::Report
|
113
|
+
element :col1, "Col 1"
|
114
|
+
element :col2, "Col 2"
|
115
|
+
grouping :col1
|
116
|
+
|
117
|
+
rows do
|
118
|
+
[
|
119
|
+
[1,2,3],
|
120
|
+
[1,4,5]
|
121
|
+
]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
it "groups data by column specified" do
|
126
|
+
report = GroupingTest.new
|
127
|
+
data = FasterCSV.parse(report.to_csv)
|
128
|
+
data.should include(['1',nil])
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe "#parameter" do
|
133
|
+
class ParameterTestReport
|
134
|
+
attr_accessor :parameter_stash
|
135
|
+
|
136
|
+
include Tabloid::Report
|
137
|
+
parameter :test_param
|
138
|
+
cache_key {"key"}
|
139
|
+
|
140
|
+
element :col1, "Column 1"
|
141
|
+
rows do
|
142
|
+
[[parameter(:test_param)]]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
it "requires a parameter in the initializer" do
|
147
|
+
expect{ ParameterTestReport.new.prepare}.should raise_error(Tabloid::MissingParameterError, "Must supply :test_param when creating the report")
|
148
|
+
end
|
149
|
+
|
150
|
+
it "makes the parameter available in the report" do
|
151
|
+
report = ParameterTestReport.new.prepare(:test_param => "supercalifragilisticexpialidocious")
|
152
|
+
report.to_html.should match(/supercalifragilisticexpialidocious/)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Tabloid::Row do
|
4
|
+
let(:columns) { [
|
5
|
+
Tabloid::ReportColumn.new(:col1, "Column 1", :hidden => true),
|
6
|
+
Tabloid::ReportColumn.new(:col2, "Column 2")
|
7
|
+
] }
|
8
|
+
let(:data) { [1, 2] }
|
9
|
+
before do
|
10
|
+
@row = Tabloid::Row.new(
|
11
|
+
:columns => columns,
|
12
|
+
:data => [1, 2]
|
13
|
+
)
|
14
|
+
end
|
15
|
+
context "producing output" do
|
16
|
+
describe "#to_csv" do
|
17
|
+
it "includes visible columns" do
|
18
|
+
rows = FasterCSV.parse(@row.to_csv)
|
19
|
+
rows.first.should include("2")
|
20
|
+
end
|
21
|
+
it "does not include hidden columns" do
|
22
|
+
rows = FasterCSV.parse(@row.to_csv)
|
23
|
+
rows.first.should_not include("1")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#to_html" do
|
28
|
+
before do
|
29
|
+
@doc = Nokogiri::HTML(@row.to_html)
|
30
|
+
end
|
31
|
+
it "should have a single row" do
|
32
|
+
(@doc / "tr").count.should == 1
|
33
|
+
end
|
34
|
+
it "should have classes on the columns" do
|
35
|
+
(@doc / "td[class='col2']").count.should == 1
|
36
|
+
end
|
37
|
+
it "should not include hidden columns" do
|
38
|
+
(@doc / "td[class='col1']").count.should == 0
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
end
|
45
|
+
context "with array data" do
|
46
|
+
describe "accessing contents with []" do
|
47
|
+
it "allows numeric access" do
|
48
|
+
@row[0].should == 1
|
49
|
+
end
|
50
|
+
it "allows access by element key" do
|
51
|
+
@row[:col1].should == 1
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
context "with object data" do
|
56
|
+
let(:data){OpenStruct.new({:col1 => 1, :col2 => 2})}
|
57
|
+
let(:row) { Tabloid::Row.new(:columns => columns, :data => data) }
|
58
|
+
describe "accessing contents with []" do
|
59
|
+
it "allows numeric access" do
|
60
|
+
row[0].should == 1
|
61
|
+
end
|
62
|
+
it "allows access by element key" do
|
63
|
+
row[:col1].should == 1
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/tabloid.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "tabloid/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "tabloid"
|
7
|
+
s.version = Tabloid::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Keith Gaddis"]
|
10
|
+
s.email = ["keith.gaddis@gmail.com"]
|
11
|
+
s.homepage = "http://github.com/Inductive/tabloid"
|
12
|
+
s.summary = %q{ Tabloid allows the creation of cacheable report data using a straightforward DSL and output to HTML, CSV, and more to come.}
|
13
|
+
s.description = %q{ Tabloid allows the creation of cacheable report data using a straightforward DSL and output to HTML, CSV, and more to come.}
|
14
|
+
|
15
|
+
s.rubyforge_project = "tabloid"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
s.add_development_dependency "rspec"
|
22
|
+
s.add_development_dependency "nokogiri"
|
23
|
+
s.add_development_dependency "dalli"
|
24
|
+
s.add_runtime_dependency "fastercsv"
|
25
|
+
s.add_runtime_dependency "builder"
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tabloid
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Keith Gaddis
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-10-03 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: nokogiri
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 3
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
type: :development
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: dalli
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 3
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
type: :development
|
61
|
+
version_requirements: *id003
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: fastercsv
|
64
|
+
prerelease: false
|
65
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
hash: 3
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
version: "0"
|
74
|
+
type: :runtime
|
75
|
+
version_requirements: *id004
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: builder
|
78
|
+
prerelease: false
|
79
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
hash: 3
|
85
|
+
segments:
|
86
|
+
- 0
|
87
|
+
version: "0"
|
88
|
+
type: :runtime
|
89
|
+
version_requirements: *id005
|
90
|
+
description: " Tabloid allows the creation of cacheable report data using a straightforward DSL and output to HTML, CSV, and more to come."
|
91
|
+
email:
|
92
|
+
- keith.gaddis@gmail.com
|
93
|
+
executables: []
|
94
|
+
|
95
|
+
extensions: []
|
96
|
+
|
97
|
+
extra_rdoc_files: []
|
98
|
+
|
99
|
+
files:
|
100
|
+
- .gitignore
|
101
|
+
- Gemfile
|
102
|
+
- LICENSE.txt
|
103
|
+
- README.txt
|
104
|
+
- Rakefile
|
105
|
+
- lib/tabloid.rb
|
106
|
+
- lib/tabloid/column_extensions.rb
|
107
|
+
- lib/tabloid/configuration.rb
|
108
|
+
- lib/tabloid/data.rb
|
109
|
+
- lib/tabloid/group.rb
|
110
|
+
- lib/tabloid/header_row.rb
|
111
|
+
- lib/tabloid/missing_element_error.rb
|
112
|
+
- lib/tabloid/missing_parameter_error.rb
|
113
|
+
- lib/tabloid/parameter.rb
|
114
|
+
- lib/tabloid/report.rb
|
115
|
+
- lib/tabloid/report_column.rb
|
116
|
+
- lib/tabloid/row.rb
|
117
|
+
- lib/tabloid/version.rb
|
118
|
+
- spec/lib/tabloid/data_spec.rb
|
119
|
+
- spec/lib/tabloid/group_spec.rb
|
120
|
+
- spec/lib/tabloid/header_row_spec.rb
|
121
|
+
- spec/lib/tabloid/report_spec.rb
|
122
|
+
- spec/lib/tabloid/row_spec.rb
|
123
|
+
- spec/spec_helper.rb
|
124
|
+
- tabloid.gemspec
|
125
|
+
homepage: http://github.com/Inductive/tabloid
|
126
|
+
licenses: []
|
127
|
+
|
128
|
+
post_install_message:
|
129
|
+
rdoc_options: []
|
130
|
+
|
131
|
+
require_paths:
|
132
|
+
- lib
|
133
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
134
|
+
none: false
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
hash: 3
|
139
|
+
segments:
|
140
|
+
- 0
|
141
|
+
version: "0"
|
142
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
143
|
+
none: false
|
144
|
+
requirements:
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
hash: 3
|
148
|
+
segments:
|
149
|
+
- 0
|
150
|
+
version: "0"
|
151
|
+
requirements: []
|
152
|
+
|
153
|
+
rubyforge_project: tabloid
|
154
|
+
rubygems_version: 1.8.6
|
155
|
+
signing_key:
|
156
|
+
specification_version: 3
|
157
|
+
summary: Tabloid allows the creation of cacheable report data using a straightforward DSL and output to HTML, CSV, and more to come.
|
158
|
+
test_files:
|
159
|
+
- spec/lib/tabloid/data_spec.rb
|
160
|
+
- spec/lib/tabloid/group_spec.rb
|
161
|
+
- spec/lib/tabloid/header_row_spec.rb
|
162
|
+
- spec/lib/tabloid/report_spec.rb
|
163
|
+
- spec/lib/tabloid/row_spec.rb
|
164
|
+
- spec/spec_helper.rb
|