dynamic_reports 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY ADDED
@@ -0,0 +1,2 @@
1
+ 0.0.0
2
+ - Initial dynamic reports codebase.
data/README ADDED
@@ -0,0 +1,146 @@
1
+ = Dynamic Reports
2
+
3
+ A dynamic reporting engine for Ruby / Rails
4
+
5
+ == Reports
6
+
7
+ The dynamic reports gem was created to fill a HUGE hole that we felt existed in the
8
+ Ruby community - the ability to QUICKLY create stylized admin reports and charts for
9
+ people to use to view key metrics and data.
10
+
11
+ Sample uses include the ability to quickly display sales data if your an eShop, our
12
+ site metrics if you are recording your own site visits, or user feedback if you are storing
13
+ feedback in a model somewhere.
14
+
15
+ Basically, with DR you can create a stylized table of ANY information found in a model
16
+ (kind of like looking at the grid output from a GUI query analyzer) as well as add Google
17
+ Charts API powered line, pie, bar or column charts of any numeric data. All this can
18
+ be done by simply creating a report definition and feeding it your data.
19
+
20
+ While this library is usable in any Ruby application it was made mainly with Rails in mind.
21
+ Suppose we have an online store and we wish to add reporting to the admin area quickly and easily.
22
+ First we define a report in app/reports/orders_report.rb, something like:
23
+
24
+ class OrdersReport < DynamicReports::Report
25
+ columns :total, :created_at
26
+ end
27
+
28
+ Then in our admin/reports controller (this can be any controller) we define an action to deliver the report:
29
+
30
+ def orders
31
+ @orders = Order.find(:all, :limit => 25)
32
+ render :text => OrdersReport.on(@orders).to_html, :layout => "application"
33
+ end
34
+
35
+ This will render an html table containing some basic styling and containing the columns 'total' and 'created_at' from the order objects.
36
+ Note that the report Title will be "Orders Report" and it's name will be :orders_report
37
+ Report#on expects that it receives an object that responds to #each and
38
+ That each object that it iterates over is either a
39
+ * An object
40
+ * A Hash
41
+ that responds to a method / has keys for each column defined within the report.
42
+
43
+
44
+ Templating engines may also be specified, currently :erb and :haml are supported (we will soon be adding :csv and :pdf) like so:
45
+
46
+ render :text => OrdersReport.on(@orders).to_html(:engine => :haml), :layout => "application"
47
+
48
+ Note that erb is the default templating engine since it is available by default in Ruby.
49
+
50
+ One may also surpress the default rendered styles you may specify that as an option as well:
51
+
52
+ render :text => OrdersReport.on(@orders).to_html(:style => false), :layout => "application"
53
+
54
+ Now let us extend our report definition to specify a template to use!
55
+
56
+ class OrdersReport < DynamicReports::Report
57
+ columns :total, :created_at
58
+ template :orders_report
59
+ end
60
+
61
+ This will look in app/views/reports/ for a template named "orders_report.html.erb" by default.
62
+ If you specify :engine => :haml then it will look for "orders_report.html.haml"
63
+
64
+ If you happen to have your report templates in a different location you can specify this as follows:
65
+
66
+ class OrdersReport < DynamicReports::Report
67
+ columns :total, :created_at
68
+ template :orders_report
69
+ views "app/views/admin/reports/"
70
+ end
71
+
72
+ And DynamicReports will look for the specified template in app/views/reports as well as app/views/admin/reports.
73
+
74
+ == Charts
75
+
76
+ Charts can be defined on a report easily. Let's say we wish to chart the total versus the item quantity sold for our Orders Report exmaple:
77
+
78
+ class OrdersReport < DynamicReports::Report
79
+ columns :total, :created_at
80
+
81
+ chart :total_vs_quantity do
82
+ columns :total, :quantity
83
+ end
84
+ end
85
+
86
+ This will render a *line* chart by default displaying the columns total and quantity.
87
+ Chart types may be specified easily:
88
+
89
+ type :bar
90
+
91
+ Available chart types are:
92
+
93
+ * :line (default)
94
+ * :bar
95
+ * :pie
96
+
97
+ Other chart types are planned.
98
+
99
+ == Rails Usage
100
+
101
+ Inside the initializer block in config/environment.rb
102
+
103
+ config.gem "dynamic_reports"
104
+
105
+ Then define your reports (as exampled above) in app/reports/*_report.rb
106
+ If you would like to customize the default report simply create your report templates
107
+ within app/views/reports/*_report.<content-type>.<engine>.
108
+
109
+ Two Rails features that we are currently working on are:
110
+
111
+ * generator
112
+ * render extensions
113
+
114
+ == Optional Dependencies
115
+
116
+ We are currently examining solutions for csv, pdf and charting.
117
+
118
+ * Fastercsv # csv
119
+ * Prawn # pdf
120
+ * flying saucer # html => PDF - if jRuby available
121
+ * amcharts # Charting, note that default is built in google charts.
122
+
123
+ These will be defined/implemented using DynamicReports plugin API (not implemented yet)
124
+ Which allows for user defined plugins of arbitrary types beyond html,csv,pdf,xml
125
+
126
+ == Contact / Feedback
127
+
128
+ If you have any suggestions on improvement please send us an email.
129
+
130
+ == Authors (alphabetically)
131
+
132
+ Joshua Lippiner (jlippiner@gmail.com)
133
+
134
+ Wayne E. Seguin (wayneeseguin@gmail.com, irc: wayneeseguin)
135
+
136
+ == Thanks To
137
+
138
+ * Daniel Neighman
139
+ * Kenneth Kalmer (And his friend :))
140
+ * Yehuda Katz
141
+
142
+ For their encouragement, feedback and advise.
143
+
144
+ == Source
145
+ http://github.com/wayneeseguin/dynamic_reports
146
+
data/gemspec.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "rubygems"
2
+
3
+ library="dynamic_reports"
4
+ version="0.0.0"
5
+
6
+ Gem::Specification::new do |spec|
7
+ $VERBOSE = nil
8
+ spec.name = library
9
+ spec.summary = library
10
+ spec.version = version
11
+ spec.description = "Dynamic Ruby Reporting Engine with support for Charts"
12
+ spec.platform = Gem::Platform::RUBY
13
+ spec.files = ["HISTORY", "README", "gemspec.rb", Dir::glob("lib/**/**")].flatten
14
+ spec.executables = Dir::glob("bin/*").map{ |script| File::basename script }
15
+ spec.require_path = "lib"
16
+ spec.has_rdoc = File::exist?("doc")
17
+ spec.author = "Wayne E. Seguin & Joshua Lippiner"
18
+ spec.email = "wayneeseguin@gmail.com, jlippiner@gmail.com"
19
+ spec.homepage = "http://github.com/wayneeseguin/direct_reports"
20
+ # spec.test_suite_file = "test/#{library}.rb" if File::directory?("test")
21
+ #spec.add_dependency "gchartrb", ">= 0.8"
22
+ spec.extensions << "extconf.rb" if File::exists?("extconf.rb")
23
+ spec.rubyforge_project = library
24
+ end
@@ -0,0 +1,47 @@
1
+ # :include: README
2
+
3
+ # Library Files
4
+ module DynamicReports
5
+ @@default_view_paths = ["#{File::dirname(File::expand_path(__FILE__))}/dynamic_reports/views/"]
6
+ def self.default_view_paths
7
+ @@default_view_paths
8
+ end
9
+ def self.default_view_paths=(paths)
10
+ @@default_view_paths = paths
11
+ end
12
+ end
13
+
14
+ require "dynamic_reports/charts"
15
+ require "dynamic_reports/reports"
16
+ require "dynamic_reports/templates"
17
+ require "dynamic_reports/views"
18
+ require "dynamic_reports/vendor/google_chart"
19
+
20
+
21
+ # TODO: Figure out why this will not load:
22
+ # require "dynamic_reports/rails"
23
+ # For now placing the code right here:
24
+ if defined?(Rails)
25
+ # Load all defined reports.
26
+ # Question: How to get Rails to reload files other than ones matching the requested constant...
27
+ #Dir.glob("#{File.join(Rails.root, "app", "reports")}/*.rb").each { |file| require file }
28
+ ActiveSupport::Dependencies.load_paths << File.join(Rails.root, "app", "reports")
29
+ # Question: Should we allow for report directory nesting ?
30
+
31
+ # Set default views directory
32
+ # TODO: These should be added to Rails views directories?
33
+ DynamicReports.default_view_paths += [
34
+ File.join(Rails.root, "app", "views", "reports"),
35
+ File.join(Rails.root, "app", "views", "layouts")
36
+ ]
37
+
38
+ # TODO: Render extension
39
+
40
+ # TODO: AR extensions:
41
+ # has_report :daily, :columns => [...], :options => {...}, :title => ...
42
+ # Which will generate a report object DailyReport with the given definition.
43
+ # Everything after the name corresponds to options available in the configuration block.
44
+
45
+ # TODO: Generator
46
+ end
47
+
@@ -0,0 +1,217 @@
1
+ require 'enumerator'
2
+
3
+ module DynamicReports
4
+ class Chart
5
+ def self.configure(name, *chart_options, &block)
6
+ chart_options = chart_options.shift || {}
7
+ chart = new(chart_options)
8
+ chart.instance_eval(&block)
9
+ chart.name name
10
+ chart
11
+ end
12
+
13
+ def initialize(*chart_options)
14
+ chart_options = chart_options.shift || {}
15
+ options.merge!(chart_options)
16
+ end
17
+
18
+ def options
19
+ @options ||= {} ; @options
20
+ end
21
+
22
+ # (Optional) Accessor used to set the chart title:
23
+ #
24
+ # title "Pageviews versus Visits"
25
+ #
26
+ # This is displayed above the chart
27
+ def title(value = nil)
28
+ value ? options[:title] = value : options[:title]
29
+ end
30
+
31
+ # Accessor used to set or get a specific report. Must be unique:
32
+ #
33
+ # name "PV_Visits"
34
+ #
35
+ def name(value = nil)
36
+ value ? options[:name] = value.to_sym : options[:name]
37
+ end
38
+
39
+ # (Optional) Accessor used by bar and pie charts to determine the
40
+ # independent varible chart labels. Due to size constraints
41
+ # this is NOT used by column or line charts, but you can add
42
+ # labels to the X-axis for those charts via chart_options and
43
+ # passing Google Chart API calls."
44
+ #
45
+ # label_column should be a SINGLE column name from the dataset.
46
+ #
47
+ # label_column "recorded_at"
48
+ #
49
+ def label_column(value = nil)
50
+ value ? options[:label_column] = value : options[:label_column]
51
+ end
52
+
53
+ # (Optional - Default = line). Accessor used to determine the
54
+ # type of chart to display. Valid options are line, column, bar
55
+ # or pie:
56
+ #
57
+ # type "bar"
58
+ #
59
+ def type(value = nil)
60
+ value ? options[:type] = value : (options[:type] || :line)
61
+ end
62
+
63
+ # (Optional - Default = 250). Accessor used to set the width, in pixels,
64
+ # of the chart.
65
+ #
66
+ # width "400"
67
+ #
68
+ def width(value = nil)
69
+ value ? options[:width] = value : (options[:width] || "250")
70
+ end
71
+
72
+ # (Optional - Default = 175). Accessor used to set the height, in pixels,
73
+ # of the chart.
74
+ #
75
+ # height "350"
76
+ #
77
+ def height(value = nil)
78
+ value ? options[:height] = value : (options[:height] || "175")
79
+ end
80
+
81
+ # (Optional - Default = false). Accessor used to determine if axis labels
82
+ # should be shown. By default y-axis labels are shown.
83
+ #
84
+ # no_labels true
85
+ #
86
+ def no_labels(value = nil)
87
+ value ? options[:no_labels] = value : options[:no_labels]
88
+ end
89
+
90
+ # (Optional) Accessor for columns
91
+ #
92
+ # Pass an array of symbols to define columns to chart. Columns MUST
93
+ # be numeric in value to chart.
94
+ #
95
+ # Example:
96
+ #
97
+ # columns :pageviews, :visits
98
+ #
99
+ # You may leave this accessor blank to default to the report columns
100
+ # specified that are numeric.
101
+ #
102
+ def columns(*array)
103
+ unless array.empty?
104
+ if (array.class == Array)
105
+ options[:columns] = array
106
+ else
107
+ raise "Report columns must be specified."
108
+ end
109
+ else
110
+ options[:columns]
111
+ end
112
+ end
113
+ end
114
+
115
+ # DynamicReports::Charts
116
+ #
117
+ # Class used to display different chart types internally. Charts are generated
118
+ # using Google Charts API
119
+ #
120
+ class Charts
121
+ class << self
122
+ # Method to select a random color from a list of hex codes
123
+ #
124
+ # Example: random_color()
125
+ # => "ff0000"
126
+ def random_color()
127
+ color_list = %w{000000 0000ff ff0000 ffff00 00ffff ff00ff 00ff00}
128
+ return color_list[rand(color_list.size)]
129
+ end
130
+
131
+ # Method to display a line chart for a given chart definition and report
132
+ def line_chart(chart, columns, report)
133
+ ::GoogleChart::LineChart.new("#{chart.width}x#{chart.height}", chart.title, false) do |c|
134
+ all_data = []
135
+ columns.each do |column|
136
+ data = []
137
+ report.records.each do |record|
138
+ if record.is_a?(Hash)
139
+ data << record[column] if record[column].is_a?(Numeric)
140
+ elsif record.respond_to?(column.to_sym)
141
+ data << record.send(column.to_sym) if record.send(column.to_sym).is_a?(Numeric)
142
+ else
143
+ data << column if column.is_a?(Numeric)
144
+ end
145
+ end
146
+ c.data column, data, random_color() unless data.empty?
147
+ all_data << data
148
+ end
149
+ all_data.flatten!
150
+ c.axis :y, :range => [all_data.min,all_data.max], :color => 'ff00ff' unless chart.no_labels
151
+ c.show_legend = columns.size > 1
152
+
153
+ return c.to_url(chart.options)
154
+ end
155
+ end
156
+
157
+ # Method to display a pie chart for a given chart definition and report
158
+ def pie_chart(chart, columns, report)
159
+ return if columns.size > 1 || chart.label_column.nil?
160
+ column = columns.first.to_s
161
+
162
+ ::GoogleChart::PieChart.new("#{chart.width}x#{chart.height}", chart.title, false) do |c|
163
+ report.records.each do |record|
164
+ if record.is_a?(Hash)
165
+ c.data record[chart.label_column.to_s], record[column] if record[column].is_a?(Numeric)
166
+ elsif record.respond_to?(column.to_sym)
167
+ c.data record.send(chart.label_column.to_s), record.send(column.to_sym) if record.send(column.to_sym).is_a?(Numeric)
168
+ else
169
+ c.data chart.label_column.to_s, column if column.is_a?(Numeric)
170
+ end
171
+ end
172
+ c.show_legend = false
173
+ c.show_labels = true
174
+
175
+ return c.to_url(chart.options)
176
+ end
177
+ end
178
+
179
+ # Method to display a bar or column chart for a given chart definition and report
180
+ def bar_column_chart(chart, columns, report, orientation)
181
+ ::GoogleChart::BarChart.new("#{chart.width}x#{chart.height}", chart.title, orientation, true) do |c|
182
+ all_data = []
183
+ all_labels = []
184
+ columns.each do |column|
185
+ data = []
186
+ report.records.each do |record|
187
+ if record.is_a?(Hash)
188
+ data << record[column] if record[column].is_a?(Numeric)
189
+ all_labels << record[chart.label_column.to_s] if chart.label_column
190
+ elsif record.respond_to?(column.to_sym)
191
+ data << record.send(column.to_sym) if record.send(column.to_sym).is_a?(Numeric)
192
+ all_labels << record.send(chart.label_column.to_s) if chart.label_column
193
+ else
194
+ data << column if column.is_a?(Numeric)
195
+ all_labels << chart.label_column.to_s if chart.label_column
196
+ end
197
+ end
198
+ c.data column, data, random_color() unless data.empty?
199
+ all_data << data
200
+ end
201
+ all_data.flatten!
202
+
203
+ if(orientation==:vertical)
204
+ c.axis :y, :range => [all_data.min,all_data.max], :color => 'ff00ff' unless chart.no_labels
205
+ else
206
+ c.axis :x, :range => [all_data.min,all_data.max], :color => 'ff00ff' unless chart.no_labels
207
+ c.axis :y, :labels => all_labels
208
+ end
209
+
210
+ c.show_legend = columns.size > 1
211
+
212
+ return c.to_url(chart.options)
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,252 @@
1
+ # DynamicReports
2
+ #
3
+ # Dynamic Reporting Engine for Ruby / Rails
4
+ module DynamicReports
5
+
6
+ # DynamicReports::Report
7
+ #
8
+ class Report
9
+ @@default_engine = "erb"
10
+
11
+ attr_accessor :name, :title, :sub_title, :columns, :charts, :records, :template, :style_name, :styles
12
+
13
+ # views accessor, array of view paths.
14
+ def views
15
+ @views
16
+ end
17
+
18
+ # options accessor, all report options and configuration is stored in this.
19
+ def options
20
+ @options
21
+ end
22
+
23
+ class << self
24
+ # Views setter and accessor.
25
+ def views(*array)
26
+ @views ||= ["#{File::dirname(File::expand_path(__FILE__))}/views/"]
27
+ array ? @views += array : @views
28
+ end
29
+
30
+ # class level options accessor
31
+ def options
32
+ @options ||= {}
33
+ end
34
+
35
+ # class level name accessor & setter
36
+ #
37
+ # Set the name of the report, for example:
38
+ #
39
+ # name "Orders Report"
40
+ #
41
+ def name(value = nil)
42
+ if value
43
+ options[:name] = value
44
+ else
45
+ # TODO: snake_case_the_name
46
+ options[:name] || self.class.name
47
+ end
48
+ end
49
+
50
+ # Accessor used to set the title:
51
+ #
52
+ # title "All orders for the account"
53
+ #
54
+ # Or to access the already set title:
55
+ #
56
+ # OrdersReport.title
57
+ #
58
+ # #=> "All orders for the account"
59
+ #
60
+ def title(value = nil)
61
+ value ? options[:title] = value : options[:title]
62
+ end
63
+
64
+ # Accessor used to set the sub title:
65
+ #
66
+ # sub_title "All orders for the account"
67
+ #
68
+ # Or to access the already set sub title:
69
+ #
70
+ # OrdersReport.sub_title
71
+ #
72
+ # #=> "All orders for the account"
73
+ #
74
+ def sub_title(value = nil)
75
+ value ? options[:sub_title] = value : options[:sub_title]
76
+ end
77
+
78
+ def styles
79
+ options[:styles] ||= false
80
+ end
81
+
82
+ def style_name(value = nil)
83
+ if value
84
+ options[:style_name] = value
85
+ elsif options[:style_name].empty?
86
+ options[:style_name] = self.to_s
87
+ else
88
+ options[:style_name]
89
+ end
90
+ end
91
+
92
+ # Accessor for the template to use for the report.
93
+ #
94
+ # Example:
95
+ #
96
+ # template :orders # => renders orders.html.erb (erb is the default engine)
97
+ #
98
+ # Used without argument returns the template set for the report.
99
+ #
100
+ # OrdersReport.template # => :orders
101
+ #
102
+ def template(value = nil)
103
+ value ? options[:template] = value : options[:template]
104
+ end
105
+
106
+ # Accessor for columns
107
+ #
108
+ # Pass an array of symbols to define columns on the report
109
+ #
110
+ # Example:
111
+ #
112
+ # columns :total, :created_at
113
+ #
114
+ # Calling the class method with no arguments will return an array with the defined columns.
115
+ #
116
+ # Example:
117
+ #
118
+ # OrdersReport.columns
119
+ #
120
+ # # => [:total, :created_at]
121
+ #
122
+ def columns(*array)
123
+ unless array.empty?
124
+ if (array.class == Array)
125
+ options[:columns] = array
126
+ else
127
+ raise "Report columns must be specified."
128
+ end
129
+ else
130
+ options[:columns]
131
+ end
132
+ end
133
+
134
+ # Return the chart with the specified name, if it exists. nil otherwise.
135
+ def chart_with_name(name)
136
+ options[:charts].to_a.detect{ |c| c.name == name.to_sym }
137
+ end
138
+
139
+ # Return an array of charts defined for the report.
140
+ def charts(object=nil)
141
+ options[:charts] ||= []
142
+ options[:charts] << object if object
143
+ options[:charts]
144
+ end
145
+
146
+ # Define a chart for the report
147
+ #
148
+ # Example:
149
+ # chart :PV_Visits, {:grxl => 'xxxxx'} do
150
+ # title "Pageviews and Visits"
151
+ # columns [:pageviews, :visits]
152
+ # no_labels false
153
+ # label_column "recorded_at"
154
+ # width "400"
155
+ # height "350"
156
+ # type "line"
157
+ # end
158
+ #
159
+ def chart(name, *chart_options, &block)
160
+ chart_options = chart_options.shift || {}
161
+ charts(Chart.configure(name, chart_options, &block))
162
+ end
163
+
164
+ # Method for instanciating a report instance on a set of given records.
165
+ #
166
+ # Example:
167
+ #
168
+ # OrdersReport.on(@records)
169
+ #
170
+ # Where @records is an array of
171
+ #
172
+ # * Objects that respond to methods for all columns defined on the report
173
+ # * Hashes that have keys corresponding to all columns defined on the report
174
+ #
175
+ # This will return an instance of the OrdersReport bound to @records
176
+ #
177
+ def on(records)
178
+ new(records, @options)
179
+ end
180
+
181
+ #--
182
+ # Methods for definining a sub report
183
+ #def link_column
184
+ #end
185
+ #def link_rows
186
+ #end
187
+
188
+ end
189
+
190
+ # Instantiate the report on a set of records.
191
+ #
192
+ # Example:
193
+ #
194
+ # OrdersReport.new(@records)
195
+ #
196
+ # options is a set of optional overrides for
197
+ #
198
+ # * views - Used to override the class defined views.
199
+ # * template - Used to override the class defined template.
200
+ #
201
+ def initialize(records, *new_options)
202
+ new_options = new_options.shift || {}
203
+ @records = records
204
+ @views = []
205
+ @views << new_options.delete(:views) if new_options[:views]
206
+ @views += self.class.views
207
+ @template = new_options.delete(:template) if new_options[:template]
208
+ @options = self.class.options.merge!(new_options)
209
+ @options.each_pair do |key,value|
210
+ if key == "chart"
211
+ # TODO: erh?
212
+ self.chart(value[:name],{},value)
213
+ else
214
+ instance_variable_set("@#{key}".to_sym, value)
215
+ end
216
+ end
217
+ end
218
+
219
+ # Convert an instance of a report bound to a records set to an html view
220
+ #
221
+ # Example
222
+ #
223
+ # OrdersReport.on(@records).to_html
224
+ #
225
+ # [options]
226
+ # :engine - one of :erb, :haml, :csv, :pdf
227
+ #
228
+ # Note: CSV & PDF forthcoming.
229
+ #
230
+ def to_html(*options)
231
+ view = View.new(self)
232
+ # todo: this is not clean...
233
+ options = (options.shift || {}).merge!(@options || {})
234
+ # todo: if rails is loaded set the default engine: dynamicreports::report.default_engine
235
+ engine = options.delete(:engine) || @@default_engine
236
+ view.__send__("#{engine}", options)
237
+ end
238
+
239
+ # Not Implemented Yet
240
+ def to_csv
241
+ # TODO: Write csv hook
242
+ end
243
+
244
+ # Not Implemented Yet
245
+ def to_pdf
246
+ # TODO: Write pdf hook
247
+ end
248
+
249
+ end
250
+
251
+ end
252
+