dynamic_reports 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+