reporter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source :rubygems
2
+ gem "arel", "~> 1.0.1"
3
+ gem "activerecord", "~> 3.0.0"
4
+ gem "activesupport", "~> 3.0.0"
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2010 Matthijs Groen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README.markdown ADDED
@@ -0,0 +1,310 @@
1
+ Reporter
2
+ ========
3
+ Reporter is a small Ruby / Rails 3.0 library for building reports. It is in heavy development right now, so documentation is sparse and code could change drastically from day to day.
4
+
5
+ Goal
6
+ ====
7
+ The goal is to create an easy to use reporting tool that not the programmer, but the client can build their own reports using data from the system.
8
+
9
+ 1. The client gets a rich AJAX web-interface to build their report data together, apply markup and store the report as a template
10
+ 2. The client gets to see up to date reporting information when they open the template, even with filter and navigation options
11
+
12
+ Design
13
+ ======
14
+ This project will consist of several different parts. The first part is to collect and query the data.
15
+ The second part is to display that data in nice views or exports.
16
+ The third part is a rich web interface so that customers can build their own reports.
17
+
18
+ The code
19
+ ========
20
+
21
+ Dependencies
22
+ ------------
23
+ As of now the code is dependent on ActiveRecord from Rails 3.
24
+
25
+ Installation and Use
26
+ --------------------
27
+ Since it is in the early development stages, there are no Gem builds available. The tool is not yet for production use, and should only be used in experimental applications. The way I develop it now is pull the project in a folder on your workstation, create a new Rails 3 project and make a symlink folder from the lib/reporter folder to the app/models/reporter folder. This is for code reloading between requests. The goal is to have a Gem in a later stadium.
28
+
29
+ Examples
30
+ ========
31
+ This code is done in the controller
32
+
33
+ # 1. Build the dataset
34
+ market_share_report = Reporter::DataSet.new do |r|
35
+ r.data_source = Reporter::DataSource.new do |data_set|
36
+
37
+ # 1a. Add the sources to extract data from
38
+ data_set << Sale << MarketshareStatistic
39
+ #Rails.logger.info data_set.scopes.possible.inspect # You can get a list of possible links between the given models
40
+
41
+ # 1b. Link the sources together on common properties
42
+ data_set.scopes.add_date_scope :time, :sale => "date", :marketshare_statistic => "date"
43
+ data_set.scopes.add_reference_scope :area, Area
44
+ end
45
+
46
+ # 1c. Add the fields / values / calculations to extract from the datasources
47
+ r.data_structure do |row|
48
+ row.add_field :period, :time
49
+ row.add_count_field :sales, :sales
50
+ row.add_sum_field :sales_workarea, :marketshare_statistics, "amount_sold"
51
+ row.add_formula_field :area_marketshare, "sales / sales_workarea"
52
+ row.add_sum_field :sales_national, :marketshare_statistics, "amount_sold", :ignore_scope => :area, :conditions => { :area_id => nil }
53
+ row.add_formula_field :national_marketshare, "sales / sales_national"
54
+ end
55
+ end
56
+
57
+ # 2. set master scopes
58
+ market_share_report.data_source.scopes.limit_scope :time, 2010 # same as Date.civil(2010).beginning_of_year .. Date.civil(2010).end_of_year
59
+ market_share_report.data_source.scopes.limit_scope :area, Company.first
60
+
61
+ @market_share = market_share # expose report for view
62
+
63
+ This code is for the view (HAML example)
64
+
65
+ %h1
66
+ Marketshare report for
67
+ = @market_share_report.scope_name :area
68
+ = @market_share_report.scope_name :time
69
+ %table.report
70
+ %thead
71
+ %th Month
72
+ %th Sales
73
+ %th Sales in area
74
+ %th Marketshare in area
75
+ %th National sales
76
+ %th National marktetshare
77
+ %tbody
78
+ - @market_share_report.iterate_time :time, :month, :quarter, :year do |row|
79
+ %tr
80
+ %td= row[:period] # The query will fire at this point. so caching makes huge profit!
81
+ %td= row[:funerals]
82
+ %td= row[:deaths_workarea]
83
+ %td= row[:workarea_marketshare].as_percentage
84
+ %td= row[:deaths_national]
85
+ %td= row[:national_marketshare].as_percentage
86
+
87
+ In detail: Building the dataset
88
+ ===============================
89
+
90
+ # 1. Build the dataset
91
+ market_share_report = Reporter::DataSet.new do |r|
92
+ r.data_source = Reporter::DataSource.new do |data_set|
93
+
94
+ # 1a. Add the sources to extract data from
95
+ data_set << Sale << MarketshareStatistic
96
+ #Rails.logger.info data_set.scopes.possible.inspect # You can get a list of possible links between the given models
97
+
98
+ Data sources and scopes
99
+ -----------------------
100
+ ActiveRecord models are added as datasources. The system will try to find common properties to scale the models against. This happens using
101
+ class methods on Scope classes (DateScope and ReferenceScope).
102
+
103
+ DateScope looks for date / time fields to lay the models next to each other.
104
+ If they find the same columnname on all models (created_at, updated_on) they will indicate an exact match. Otherwise it suggests a loose match. The strictness of the match is not relevant for building and linking the dataset, but an indicator for building a UI.
105
+
106
+ ReferenceScope looks for the same type of Object association on all models. if all models have an _belongs_to :area_ method, the ReferenceScope will suggest a link through "area". You can even have a has_one relationship through a belongs to.
107
+ So:
108
+ has_one :area, :through => :sale_area
109
+ is supported.
110
+
111
+ # 1b. Link the sources together on common properties
112
+ data_set.scopes.add_date_scope :time, :sale => "date", :marketshare_statistic => "date"
113
+ data_set.scopes.add_reference_scope :area, Area
114
+ end
115
+
116
+ After the suggestions (which in code you won't use actively) you have to set the scopes on the properties, and decide wich fields to link together.
117
+ In the reference example, all models only have one association to the Area object, so no specific columname is needed. (it will figure that out itself)
118
+
119
+ We have one big pool of data, wich is filterable through scopes (time and area in this case)
120
+ Now we have to tell how to extract the data from the set. Calculations are defined here.
121
+
122
+ # 1c. Add the fields / values / calculations to extract from the datasources
123
+ r.data_structure do |row|
124
+ row.add_field :period, :time
125
+ row.add_count_field :sales, :sales
126
+ row.add_sum_field :sales_workarea, :marketshare_statistics, "amount_sold"
127
+ row.add_formula_field :area_marketshare, "sales / sales_workarea"
128
+ row.add_sum_field :sales_national, :marketshare_statistics, "amount_sold", :ignore_scope => :area, :conditions => { :area_id => nil }
129
+ row.add_formula_field :national_marketshare, "sales / sales_national"
130
+ end
131
+ end
132
+
133
+ Field types
134
+ -----------
135
+ As you can see there are several different field types to add.
136
+
137
+ *Field* If passed a regular value, this value is set for the field with the given name.
138
+ If passed a Symbol, the name of the scope of the symbol will be used as value
139
+ you can even pass a block an create a custom field with own querie mechanisms or function calls
140
+
141
+ *CountField* counts the records of a given datasource (second parameter) the datasource is here used in plural. (don't know if it will stay this way). An optional hash with conditions can be provided.
142
+
143
+ *SumField* similar to the CountField, this fieldtype will sum up all results from a column of a given datasource (datasource is param 3, column is param 4)
144
+ in the line :sales_national you can see the use of :ignore_scope and an additional conditions hash.
145
+
146
+ *:ignore_scope* before retrieval of field data the correct scopes will be built for query execution. If certain scopes must be ignored for certain fields, you can use
147
+ :ignore_scope => :scope_name or :ignore_scopes => [:scope, :scope]
148
+
149
+ *FormulaField* the formula field is for simple calculations. variable names used will be retrieved from their active row.
150
+
151
+
152
+ Limits
153
+ ------
154
+ At last default limits are set for the scopes. This can be overridden (temporarily) by iterators used in views.
155
+
156
+ # 2. set master scopes
157
+ market_share_report.data_source.scopes.limit_scope :time, 2010 # same as Date.civil(2010).beginning_of_year .. Date.civil(2010).end_of_year
158
+ market_share_report.data_source.scopes.limit_scope :area, Company.first
159
+
160
+ @market_share = market_share # expose report for view
161
+
162
+ The value passed is interpreted by their respective scope, so 2010 as value for the DateScope will be translated to Date.civil(2010).beginning_of_year .. Date.civil(2010).end_of_year
163
+ The same is for ReferenceScope. An object (or list of them) passed that are not matches for the required object (Area in this case) will be investigated for a link.
164
+ In this case, a Company has_many :areas. All the areas of the company will be used as scope.
165
+
166
+
167
+ In detail: Building the view
168
+ ============================
169
+
170
+ %h1
171
+ Marketshare report for
172
+ = @market_share_report.scope_name :area
173
+ = @market_share_report.scope_name :time
174
+
175
+ Scope name will print a human readable name for the currently active scope. In this case, :area will print the company name, and :time will print "2010"
176
+
177
+ %table.report
178
+ %thead
179
+ %th Month
180
+ %th Sales
181
+ %th Sales in area
182
+ %th Marketshare in area
183
+ %th National sales
184
+ %th National marktetshare
185
+ %tbody
186
+ - @market_share_report.iterate_time :time, :month, :quarter, :year do |row|
187
+
188
+ Iteration
189
+ ---------
190
+ Report supports 2 different iterators (for now). A normal iterator that loops through a set of objects (eg. a list of Areas or Companies could be used) or the
191
+ time iterator. The time iterator must be provided with the name of the DateScope, and additional arguments for the chunks of time to iterate.
192
+ :month will iterate the limit period in chunks of 1 month, :quarter in chunks of 3 months, :year in chunks of a year.
193
+
194
+ The order of the supplied parameters is important. if you supply: :month, :quarter, :year for the first half of 2010, the periods will be as follows:
195
+ Jan 10, Feb 10, Mar 10, Q1 10, Apr 10, May 10, Jun 10, Q2 10, 2010
196
+
197
+ If you supply these parameters in the order of :year, :month, :quarter, the periods will be as followed:
198
+ 2010, Jan 10, Feb 10, Mar 10, Q1 10, Apr 10, May 10, Jun 10, Q2 10
199
+
200
+ If you pass :year, :quarter, :month, the quarters will be placed before their containing months.
201
+
202
+ not all combinations are valid however. The arguments are parsed into a tree form, described as follows:
203
+ :total => 6, :year => 5, :quarter => 4, :month => 3, :week => 2, :day => 1
204
+
205
+ the outsides of the series must always have the biggest value of the set.
206
+ valid: [:year(5), :month(3), :quarter(4)] == :year(pre) => :quarter(post) => :month(nil)
207
+ valid: [:year(5), :month(3), :week(2), :quarter(4)] == :year(pre) => :quarter(post) => :month(pre) => :week(nil)
208
+ invalid: [:year(5), :month(3), :quarter(4), :week(2)] The childs of year (biggest in initial set) are: [:month(3), :quarter(4), :week(2)] The larges value is not the first or last item, so this set is invalid.
209
+
210
+ %tr
211
+ %td= row[:period] # The query will fire at this point. so caching makes huge profit!
212
+ %td= row[:funerals]
213
+ %td= row[:deaths_workarea]
214
+ %td= row[:workarea_marketshare].as_percentage
215
+ %td= row[:deaths_national]
216
+ %td= row[:national_marketshare].as_percentage
217
+
218
+ the [] method in the row will execute the scopes and ask the field to calculate the value. the value is cached so multiple uses of the same value will not decrease performance (since field values can also be accessed by formulas). The Result is an ReportValue object that support several formatting options and meta data.
219
+
220
+ Advanced example
221
+ ================
222
+ I will not cover this in detail, but here a far more complex example of a employee capacity report:
223
+
224
+ def capacity_report
225
+ # 1. Build the dataset
226
+ capacity_report = Reporter::DataSet.new do |r|
227
+ r.data_source = Reporter::DataSource.new do |data_set|
228
+ # 1a. Add the sources to extract data from
229
+ data_set << Funeral << Employee << TimeRegistration
230
+ Rails.logger.info data_set.scopes.possible.inspect
231
+ # 1b. Link the sources together on common properties
232
+ data_set.scopes.add_date_scope :time, :funeral => "notification", :time_registration => "date"
233
+ data_set.scopes.add_reference_scope :company, Company
234
+ end
235
+
236
+ # 1c. Add the fields / values / calculations to extract from the datasources
237
+ r.data_structure do |row|
238
+ row.add_field :period, :time
239
+ row.add_count_field :funerals, :funerals
240
+ row.add_formula_field :funeral_hours, "2080 / 108"
241
+ row.add_formula_field :funeral_time, "funerals * funeral_hours"
242
+
243
+ row.add_field :fte do |data_source, options, result_row|
244
+ active_period = data_source.scopes.get(:time).active_period
245
+ conditions = ["(funeral_organizer = ? OR funeral_caretaker = ?) AND parttime = ? AND internal = ? AND start_date <= ? AND (end_date >= ? OR end_date IS NULL)",
246
+ true, true, false, true, active_period.end, active_period.begin]
247
+
248
+ db_start = active_period.begin.to_s(:db)
249
+ db_end = active_period.end.to_s(:db)
250
+ source = data_source.get(:employees)
251
+ value = source.sum "datediff(least(ifnull(end_date, '#{db_end}'), '#{db_end}'), " +
252
+ "greatest(ifnull(start_date, '#{db_start}'), '#{db_start}')) + 1", :ignore_scope => :time,
253
+ :conditions => conditions
254
+ result_row.value = value.to_f
255
+ end
256
+ row.add_formula_field :internal_hours, "fte * (40 / 7.0)"
257
+ row.add_sum_field :external_hours, :time_registrations, :hours
258
+ row.add_formula_field :total_hours, "internal_hours + external_hours"
259
+ row.add_formula_field :capacity, "funeral_time / total_hours"
260
+ end
261
+ end
262
+
263
+ # 2. set master scopes
264
+ capacity_report.data_source.scopes.limit_scope :time, 2010
265
+ capacity_report.data_source.scopes.limit_scope :company, Company.all
266
+
267
+ @capacity_report = capacity_report
268
+ end
269
+
270
+
271
+ The view, that also changes the scopes during iteration:
272
+
273
+ %h1
274
+ Capaciteits rapport
275
+ = @capacity_report.scope_name :time
276
+ %table.report
277
+ %thead
278
+ %th Maand
279
+ %th Uv
280
+ %th Uren
281
+ %th Werkelijk
282
+ %th Intern
283
+ %th Extern
284
+ %th %
285
+
286
+ %th Cum. Uv
287
+ %th Cum. Uren
288
+ %th Cum. Werkelijk
289
+ %th Cum. Intern
290
+ %th Cum. Extern
291
+ %th %
292
+ %tbody
293
+ - @capacity_report.iterate_time :time, :month do |row|
294
+ %tr
295
+ %td= row[:period]
296
+ %td= row[:funerals]
297
+ %td= row[:funeral_time].round 2
298
+ %td= row[:total_hours].round 2
299
+ %td= row[:internal_hours].round 2
300
+ %td= row[:external_hours].round 2
301
+ %td= row[:capacity].as_percentage
302
+
303
+ - cum_row = @capacity_report.get_row :time => :year_cumulative
304
+ %td= cum_row[:funerals]
305
+ %td= cum_row[:funeral_time].round 2
306
+ %td= cum_row[:total_hours].round 2
307
+ %td= cum_row[:internal_hours].round 2
308
+ %td= cum_row[:external_hours].round 2
309
+ %td= cum_row[:capacity].as_percentage
310
+
data/Rakefile ADDED
@@ -0,0 +1,62 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "reporter"
8
+ gem.summary = %Q{Report builder.}
9
+ gem.description = %Q{
10
+ Reporter adds a consistent way to build reports.
11
+ }
12
+ gem.email = "matthijs.groen@gmail.com"
13
+ gem.homepage = "http://github.com/matthijsgroen/reporter"
14
+ gem.authors = ["Matthijs Groen"]
15
+ #gem.add_development_dependency "shoulda"
16
+ gem.add_dependency "activerecord", "~> 3.0.0"
17
+ gem.add_dependency "activesupport", "~> 3.0.0"
18
+ gem.add_dependency "arel", "~> 1.0.1"
19
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
20
+ end
21
+ Jeweler::GemcutterTasks.new
22
+ rescue LoadError
23
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
24
+ end
25
+
26
+ require 'rake/testtask'
27
+ Rake::TestTask.new(:test) do |test|
28
+ test.libs << 'lib' << 'test'
29
+ test.libs << 'vendor/rails/activerecord/lib'
30
+ test.libs << 'vendor/rails/activesupport/lib'
31
+ test.libs << 'vendor/arel/lib'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ end
35
+
36
+ begin
37
+ require 'rcov/rcovtask'
38
+ Rcov::RcovTask.new do |test|
39
+ test.libs << 'test'
40
+ test.pattern = 'test/**/test_*.rb'
41
+ test.verbose = true
42
+ end
43
+ rescue LoadError
44
+ task :rcov do
45
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
46
+ end
47
+ end
48
+
49
+ # Don't check dependencies since we're testing with vendored libraries
50
+ # task :test => :check_dependencies
51
+
52
+ task :default => :test
53
+
54
+ require 'rake/rdoctask'
55
+ Rake::RDocTask.new do |rdoc|
56
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
57
+
58
+ rdoc.rdoc_dir = 'rdoc'
59
+ rdoc.title = "meta_where #{version}"
60
+ rdoc.rdoc_files.include('README*')
61
+ rdoc.rdoc_files.include('lib/**/*.rb')
62
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,53 @@
1
+ require "reporter/support/time_range"
2
+
3
+ # DataSet is where all information about the data for the report comes together.
4
+ #
5
+ class Reporter::DataSet
6
+
7
+ def initialize *args
8
+ @row_structure = nil
9
+ @axis_values = {}
10
+
11
+ @row_cache = {}
12
+ yield self if block_given?
13
+ end
14
+
15
+ def data_source= value
16
+ #TODO Maybe add checks, maybe make the datasource an internal part of the reporting dataset
17
+ @data_source = value
18
+ end
19
+
20
+ attr_reader :data_source
21
+
22
+ # creation of the definition of the available fields/values to retrieve from the various datasources
23
+ def data_structure *args, &block
24
+ @data_structure ||= Reporter::DataStructure.new self, *args
25
+ yield @data_structure if block_given?
26
+ @data_structure
27
+ end
28
+
29
+ def get_row options = {}
30
+ # The datastructure of a resultrow it is a container for caching result values so that
31
+ # formula's can retrieve values to perform calculations. Queries are executed as late as
32
+ # possible. This way template caching can eliminate performing database queries altogether.
33
+ current_scope = data_source.scopes.change(options).current_scope
34
+ @row_cache[current_scope.hash] ||= Reporter::ResultRow.new(self, current_scope)
35
+ end
36
+
37
+ # iterate the chosen scope over a list of selected items. If no items are provided the defined
38
+ # maximum and minimum limit is used. (scope.set_limit)
39
+ # use iterate_time to iterate over time periods.
40
+ def iterate scope, items = nil, &block
41
+ raise "No data-source set" unless data_source
42
+ data_source.scopes.get(scope).iterate items, self, &block
43
+ end
44
+
45
+ # returns the name of the current active scope. This can be used to decorate report data with the proper context
46
+ def scope_name scope
47
+ raise "No data-source set" unless data_source
48
+ data_source.scopes.get(scope).human_name
49
+ end
50
+
51
+ include Reporter::TimeIterator
52
+
53
+ end
@@ -0,0 +1,136 @@
1
+ # ActiveRecordSource supplies information about the datasources and executes the queries upon it.
2
+ # The source holds the active record model to build queries upon.
3
+ #
4
+ class Reporter::DataSource::ActiveRecordSource
5
+
6
+ def initialize(data_source, active_record)
7
+ @data_source = data_source
8
+ @active_record = active_record
9
+ @name = active_record.name.pluralize.underscore.to_sym
10
+ end
11
+
12
+ attr_reader :active_record, :name
13
+
14
+ # returns all date columns
15
+ def date_columns
16
+ load_columns if @date_columns.nil?
17
+ @date_columns
18
+ end
19
+
20
+ # returns all associations, grouped by the type of model linked to.
21
+ def relations_to_objects
22
+ load_columns if @object_links.nil?
23
+ @object_links
24
+ end
25
+
26
+ # display and inspection
27
+ def inspect
28
+ active_record.inspect
29
+ end
30
+
31
+ def model_name
32
+ active_record.name
33
+ end
34
+
35
+ VALID_CALCULATIONS = %w(count sum average minimum maximum)
36
+
37
+ # retrieve data from source
38
+ def calculate calculation, *args, &block
39
+ raise "invalid calculation: #{calculation}" unless VALID_CALCULATIONS.include? calculation.to_s
40
+ # TODO: include check for valid calculations and required parameters
41
+ options = args.extract_options!.dup
42
+ scope_options = extract_scope_options_from options
43
+ source = source_with_applied_scopes(scope_options)
44
+ source = block.call(source, data_source.scopes) if block_given?
45
+ source.send *([calculation] + args + [options])
46
+ end
47
+
48
+ # performs a calculation for an entire period, in groups of months, years, weeks, quarters or days
49
+ def calculate_for_period calculation, period, filter, scope, *args, &block
50
+ options = args.extract_options!.dup
51
+ # remove the time scope from the default scopes
52
+ scope_options = extract_scope_options_from options
53
+ scope_options[:ignore_scopes] << scope.name.to_sym
54
+ scope_options[:ignore_scopes].uniq!
55
+ source = source_with_applied_scopes(scope_options)
56
+
57
+ source = block.call(source, data_source.scopes) if block_given?
58
+
59
+ # add time scope seperately with full period
60
+ source = scope.apply_on source, period
61
+
62
+ # group on the given filters (year and month, in case of months)
63
+ grouping = filter.collect { |f| scope.group_on source, f }
64
+ source = source.group grouping.join(", ")
65
+
66
+ # set the columns for the selection. We have to do this in manual SQL, since Rails does not
67
+ # support multiple group by's in the calculation functions.
68
+ select = []
69
+ select << calculation_function(calculation, args)
70
+ filter.each_with_index { |f, index| select << "#{grouping[index]} as #{f.to_s}" }
71
+ source = source.select select
72
+ #Rails.logger.info source.to_sql
73
+
74
+ # execute the query and collect the results
75
+ # place all results in an collection hash with their filter attributes as key
76
+ result = source.collect do |r|
77
+ result = r.result.to_f
78
+ result = result.to_i if result.floor == result
79
+ res = { :value => result }
80
+ filter.each { |f| res[f] = r[f.to_s].to_i }
81
+ res
82
+ end
83
+ result
84
+ end
85
+
86
+ # apply all scopes on the active record model
87
+ def source_with_applied_scopes(options)
88
+ #Rails.logger.info options.inspect
89
+ data_source.scopes.apply_on(active_record, options)
90
+ end
91
+
92
+ private
93
+
94
+ attr_reader :data_source
95
+
96
+ # examine the active record model and store all relevant data for scoping
97
+ def load_columns
98
+ @date_columns = []
99
+ active_record.columns.collect do |column|
100
+ if [Time, Date].include? column.klass
101
+ @date_columns << column.name
102
+ end
103
+ end.compact
104
+
105
+ @object_links = {}
106
+ active_record.reflect_on_all_associations.each do |reflection|
107
+ @object_links[reflection.klass] ||= []
108
+ @object_links[reflection.klass] << reflection.name.to_s
109
+ end
110
+ end
111
+
112
+ def extract_scope_options_from options
113
+ scope_options = {}
114
+ scope_options[:ignore_scopes] = options.delete(:ignore_scopes) || []
115
+ scope_options[:ignore_scopes] += [options.delete :ignore_scope] if options[:ignore_scope]
116
+ scope_options
117
+ end
118
+
119
+ def calculation_function(calculation, args)
120
+ case calculation
121
+ when :sum :
122
+ "SUM(#{args.first}) AS result"
123
+ when :count :
124
+ "COUNT(*) AS result"
125
+ when :average :
126
+ "AVG(#{args.first}) AS result"
127
+ when :minimum :
128
+ "MIN(#{args.first}) AS result"
129
+ when :maximum :
130
+ "MAX(#{args.first}) AS result"
131
+ else
132
+ raise "Invalid calculation: #{calculation}"
133
+ end
134
+ end
135
+
136
+ end