simple_drilldown 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 96c716479c1f71ab063f35abb76e07fd5c653349
4
+ data.tar.gz: 74e1c9d74a74da0cddcdee31b923f9e6726319c5
5
+ SHA512:
6
+ metadata.gz: a52cc53302441f70ebb754d2340d4ae10d7431900c418b5a0e31288ecbd225b5b98a09cbbf25c39a9c0e2eec1c14d00da28a5ac9ea89de98810cf537d0ac321e
7
+ data.tar.gz: 4c0f6fd6715508b49ff04b8e5fa1bba6a558d35eb2fec733ce7bb2a31dc8c58a0c67a6745fc9c6f4fa7f2f0a84932d59caa3c9f17ec0a4be3ffb2b52ddd0ff23
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *~
2
+ \#*
3
+ .\#*
4
+ *.gem
5
+ *.rbc
6
+ .bundle
7
+ .config
8
+ .DS_Store
9
+ /.idea
10
+ .yardoc
11
+ _yardoc
12
+ coverage
13
+ doc/
14
+ Gemfile.lock
15
+ InstalledFiles
16
+ lib/bundler/man
17
+ pkg
18
+ rdoc
19
+ spec/reports
20
+ test/tmp
21
+ test/version_tmp
22
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,18 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ simple_drilldown (0.0.1)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ rake (10.1.0)
10
+
11
+ PLATFORMS
12
+ java
13
+ ruby
14
+
15
+ DEPENDENCIES
16
+ bundler (~> 1.3)
17
+ rake
18
+ simple_drilldown!
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2011-2013 Datek Wireless AS
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # SimpleDrilldown
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'simple_drilldown'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install simple_drilldown
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = simple_drilldown
2
+
3
+ An analysis framework for active record.
4
+
5
+ == Contributing to simple_drilldown
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
9
+ * Fork the project
10
+ * Start a feature/bugfix branch
11
+ * Commit and push until you are happy with your contribution
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2011 donv. See LICENSE.txt for
18
+ further details.
19
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,95 @@
1
+ class SampleDrilldownController < DrilldownController
2
+
3
+ default_fields %w{time receipt operation flight aircraft_registration stand volume_abbr volume_compensated payment_type supplier}
4
+ target_class Transaction
5
+ select "sum(CASE WHEN operation = 'FUELLING' THEN #{Volume::Sql::VOLUME} ELSE -#{Volume::Sql::VOLUME} END) as volume, sum(CASE WHEN operation = 'FUELLING' THEN #{Volume::Sql::VOLUME_COMPENSATED} ELSE -#{Volume::Sql::VOLUME_COMPENSATED} END) as volume_compensated, sum(CASE WHEN operation = 'CREDIT' THEN -1 ELSE 1 END) as count".freeze
6
+ list_includes :operator, :stand, :supplier, :vehicle
7
+ list_order 'transactions.created_at'
8
+
9
+ field :aircraft_registration, :last_change_time => true
10
+ field :aircraft_subtype, :attr_method => lambda { |transaction| transaction.aircraft_subtype_code }
11
+ field :airfield_fee => {:excel_type => 'Number', :excel_style => 'ThreeDecimalNumberFormat'},
12
+ field :authorization_reference => { :attr_method => lambda { |transaction| transaction.authorization.try(:authorization_reference) }},
13
+ field :carnet_no => {},
14
+ field :cash_price => {:excel_type => 'Number', :excel_style => 'ThreeDecimalNumberFormat'},
15
+ field :co2_fee => {:excel_type => 'Number', :excel_style => 'ThreeDecimalNumberFormat'},
16
+ field :comment => {},
17
+ # :customer_name => {},
18
+ # :defuelling_fee => {:excel_type => 'Number',
19
+ # :excel_style => 'ThreeDecimalNumberFormat'},
20
+ # :delay_codes => {},
21
+ # :density => {:excel_type => 'Number',
22
+ # :excel_style => 'StandardNumberFormat'},
23
+ # :destination => {},
24
+ # :dispatched => { :attr_method => lambda { |t| t.assignment.try(:created_at).try(:localtime).try(:strftime, '%H:%M') } },
25
+ # :external => { :attr_method => lambda { |transaction| transaction.external_flight }},
26
+ # :flight =>{ :attr_method => lambda { |transaction| transaction.flight_no } },
27
+ # :fuel_request => { :attr_method => lambda { |t| t.assignment.try(:fuel_request)}, :last_change_time => true, :include => {:assignment => :order}},
28
+ # :invoice_code => { :attr_method => lambda { |transaction| transaction.contract.try(:invoice_code)}},
29
+ # :ofb => { :attr_method => lambda { |t| t.assignment.try(:order).try(:ofb).try(:localtime).try(:strftime, '%H:%M') } },
30
+ # :onb => { :attr_method => lambda { |t| t.assignment.try(:order).try(:onb).try(:localtime).try(:strftime, '%H:%M') } },
31
+ # :operation => {},
32
+ # :operator_abbr => { :attr_method => lambda { |transaction| transaction.operator.try(:login) } },
33
+ # :payment_type => {},
34
+ # :product => {},
35
+ # :ptd => { :attr_method =>lambda { |transaction| transaction.assignment.try(:order).try(:ptd).try(:localtime).try(:strftime, '%H:%M') } },
36
+ # :receipt => { :attr_method => lambda { |transaction| transaction.receipt_code } },
37
+ # # Not paid for, yet.
38
+ # # :receptacle => {},
39
+ # :receptacle_fee => {:excel_type => 'Number',
40
+ # :excel_style => 'ThreeDecimalNumberFormat'},
41
+ # :remarks => { :attr_method => lambda { |t| t.assignment.try(:order).try(:remarks) } },
42
+ # :remote_fee => { :attr_method => lambda { |transaction| transaction.remote_fuelling_fee },
43
+ # :excel_type => 'Number',
44
+ # :excel_style => 'ThreeDecimalNumberFormat'},
45
+ # :sta => { :attr_method =>lambda { |t| t.assignment.try(:order).try(:sta).try(:localtime).try(:strftime, '%H:%M') } },
46
+ # :stand => { :attr_method =>lambda { |transaction| transaction.stand.try(:code) } },
47
+ # :started => { :attr_method =>lambda { |t| t.assignment.try(:started_at).try(:localtime).try(:strftime, '%H:%M') } },
48
+ # :std => { :attr_method =>lambda { |transaction| transaction.assignment.try(:order).try(:std).try(:localtime).try(:strftime, '%H:%M') } },
49
+ # :supplier => { :attr_method => lambda { |transaction| transaction.supplier.try(:name) }},
50
+ # :supplier_price => {:excel_type => 'Number',
51
+ # :excel_style => 'ThreeDecimalNumberFormat'},
52
+ # :temperature => {:excel_type => 'Number',
53
+ # :excel_style => 'StandardNumberFormat'},
54
+ # :time => {},
55
+ # :vat_factor => {:excel_type => 'Number',
56
+ # :excel_style => 'ThreeDecimalNumberFormat'},
57
+ # :vehicle => { :attr_method => lambda { |transaction| transaction.vehicle.try(:name) }},
58
+ # :volume_abbr => { :attr_method => lambda { |transaction| transaction.volume },
59
+ # :excel_type => 'Number' },
60
+ # :volume_compensated => {:excel_type => 'Number'},
61
+ # :zero_fuelling_fee => {:excel_type => 'Number',
62
+ # :excel_style => 'ThreeDecimalNumberFormat'},
63
+ # }
64
+
65
+ dimension :calendar_date, "DATE(transactions.created_at AT TIME ZONE 'CET0')", :interval => true
66
+ dimension :comment, "CASE WHEN (transactions.comment is not null AND transactions.comment <> '') THEN 'Yes' ELSE 'No' END"
67
+ dimension :corrected, "CASE WHEN meter1_start_volume_manual is not null OR meter1_stop_volume_manual is not null OR meter2_start_volume_manual is not null OR meter2_stop_volume_manual is not null THEN 'Yes' ELSE 'No' END"
68
+ dimension :customer, 'COALESCE(customers.name, customer_name)', :includes => {:contract => :customer}
69
+ dimension :day_of_month, "date_part('day', transactions.created_at AT TIME ZONE 'CET0')"
70
+ dimension :day_of_week, "CASE WHEN date_part('dow', transactions.created_at AT TIME ZONE 'CET0') = 0 THEN 7 ELSE date_part('dow', transactions.created_at AT TIME ZONE 'CET0') END",
71
+ :label_method => lambda { |day_no| Date::DAYNAMES[day_no.to_i % 7] }
72
+ dimension :delay_codes, "transactions.delay_codes IS NOT NULL AND (transactions.delay_codes LIKE '%GF36%')",
73
+ :label_method => lambda { |val| val == 't' || val == 'true' ? 'With' : 'Without' },
74
+ :legal_values => lambda { | val | [['With', 't'], ['Without', 'f']]}
75
+ dimension :destination, "CASE WHEN external_flight = 't' THEN '#{t(:international)}' ELSE '#{t(:domestic)}' END"
76
+ dimension :hour_of_day, "date_part('hour', transactions.created_at AT TIME ZONE 'CET0')"
77
+ dimension :manual, "CASE WHEN receipt_no < #{Transactional::MANUAL_RECEIPT_NO_LIMIT} THEN 'Automatic' ELSE 'Manual' END"
78
+ dimension :month, "date_part('month', transactions.created_at AT TIME ZONE 'CET0')",
79
+ :label_method => lambda { |month_no| Date::MONTHNAMES[month_no.to_i] }
80
+ dimension :operation, 'operation'
81
+ dimension :operator, 'users.login', :includes => :operator
82
+ dimension :payment_type, 'payment_type'
83
+ dimension :pit, 'pits.code', :includes => :pit
84
+ # Not paid for yet: 4h
85
+ # dimension :receptacle, 'receptacle'
86
+ dimension :stand, 'stands.code', :includes => :stand
87
+ dimension :supplier, 'suppliers.name', :includes => :supplier
88
+ dimension :terminal, "CASE WHEN stands.code like '3__%' THEN 'GA' WHEN stands.code like 'M%' THEN 'Military' WHEN stands.code like 'PAD' THEN 'PAD' ELSE 'Terminal 1' END",
89
+ :includes => :stand, :label_method => lambda { |val| val }
90
+ dimension :vehicle, 'vehicles.name', :includes => :vehicle
91
+ dimension :week, "date_part('week', transactions.created_at AT TIME ZONE 'CET0')"
92
+ dimension :year, "date_part('year', transactions.created_at AT TIME ZONE 'CET0')"
93
+ dimension :zero_fuelling, "CASE WHEN zero_fuelling_fee is not null THEN 'Zero' ELSE 'Non-zero' END"
94
+
95
+ end
@@ -0,0 +1,318 @@
1
+ class DrilldownController < ApplicationController
2
+ def initialize(fields, default_fields, target_class, select, list_includes, list_order)
3
+ super()
4
+ @fields = fields
5
+ @default_fields = default_fields
6
+ @target_class = target_class
7
+ @select = select
8
+ @list_includes = list_includes
9
+ @list_order = list_order
10
+ end
11
+
12
+ def xml_export
13
+ params[:search][:list] = '1'
14
+ index(false)
15
+ @transactions = get_transactions(@result)
16
+ headers['Content-Type'] = "text/xml"
17
+ headers['Content-Disposition'] = 'attachment; filename="transactions.xml"'
18
+ render :template => '/transaction_drilldown/xml_export', :layout => false
19
+ end
20
+
21
+ # ?dimension[0]=supplier&dimension[1]=transaction_type&
22
+ # filter[year]=2009&filter[supplier][0]=Shell&filter[supplier][1]=Statoil
23
+ def index(do_render = true)
24
+ @search = Search.new(params[:search], @default_fields)
25
+
26
+ @transaction_fields = (@search.fields + ((@fields.keys.map{ |field_name| field_name.to_s }) - @search.fields))
27
+ @transaction_fields_map = @fields
28
+
29
+ select = @select.dup
30
+ includes = []
31
+
32
+ @dimensions = []
33
+ select << ", 'All' as value0"
34
+ @dimensions += @search.dimensions.map do |dn|
35
+ raise "Unknown distribution field: #{@search.dimensions.inspect}" if @dimension_defs[dn].nil?
36
+ @dimension_defs[dn]
37
+ end
38
+ @dimensions.each_with_index do |d, i|
39
+ select << ", #{d[:select_expression]} as value#{i + 1}"
40
+ includes << d[:includes] if d[:includes]
41
+ end
42
+
43
+ conditions, @filter_text, filter_includes = make_conditions(@search.filter)
44
+ includes += filter_includes
45
+ includes.uniq!
46
+ if @search.order_by_value && @dimensions.size <= 1
47
+ order = @search.select_value == Search::SelectValue::VOLUME ? 'volume DESC' : 'count DESC'
48
+ else
49
+ order = @dimensions.map{|d| d[:select_expression]}.join(', ')
50
+ order = nil if order.empty?
51
+ end
52
+ group = @dimensions.map{|d| d[:select_expression]}.join(', ')
53
+ group = nil if group.empty?
54
+
55
+ rows = @target_class.find(
56
+ :all, :select => select, :conditions => conditions,
57
+ :joins => make_join(@target_class.name.underscore.to_sym, includes), :group => group,
58
+ :order => order
59
+ )
60
+
61
+ if rows.empty?
62
+ @result = {:value=>"All", :count=>0, :volume=>0, :volume_compensated=>0, :row_count=>0, :nodes=>0, :rows=>[]}
63
+ else
64
+ @result = result_from_rows(rows, 0, 0, ['All'])
65
+ end
66
+ @search.list = false if @result[:count] > 10000
67
+
68
+ @remaining_dimensions = @dimension_defs.dup.delete_if{|dim_name, dimension| @search.filter[dim_name] && @search.filter[dim_name].size == 1}
69
+ populate_list(conditions, includes, @result, []) if @search.list
70
+ render :template => '/transaction_drilldown/index' if do_render
71
+ end
72
+
73
+ # Empty summary rows are needed to plot zero points in the charts
74
+ def add_zero_results(result_rows, dimension)
75
+ legal_values = legal_values_for(@dimensions[dimension][:url_param_name], true).call(@search).map{|lv| lv[1]}
76
+ current_values = result_rows.map{|r| r[:value]}.compact
77
+ empty_values = legal_values - current_values
78
+
79
+ unless empty_values.empty?
80
+ empty_values.each do | v |
81
+ sub_result = {
82
+ :value => v,
83
+ :count => 0,
84
+ :volume => 0,
85
+ :volume_compensated => 0,
86
+ :row_count => 0,
87
+ :nodes => 0,
88
+ }
89
+ if dimension < @dimensions.size - 1
90
+ sub_result[:rows] = add_zero_results([], dimension + 1)
91
+ end
92
+ result_rows << sub_result
93
+ end
94
+ result_rows = result_rows.sort_by { |r| legal_values.index(r[:value]) }
95
+ end
96
+ result_rows
97
+ end
98
+
99
+ def result_from_rows(rows, row_index, dimension, previous_values)
100
+ row = rows[row_index]
101
+ return nil if row.nil?
102
+ values = (0..dimension).to_a.map{|i| row["value#{i}"]}
103
+ return nil if values != previous_values
104
+
105
+ if dimension == @dimensions.size
106
+ return {
107
+ :value => values[-1],
108
+ :count => row[:count].to_i,
109
+ :volume => row[:volume].to_i,
110
+ :volume_compensated => row[:volume_compensated].to_i,
111
+ :row_count => 1,
112
+ :nodes => 1,
113
+ }
114
+ end
115
+
116
+ result_rows = []
117
+ loop do
118
+ sub_result = result_from_rows(rows, row_index, dimension + 1, values + [rows[row_index]["value#{dimension + 1}"]])
119
+ break if sub_result.nil?
120
+ result_rows << sub_result
121
+ row_index += sub_result[:row_count]
122
+ break if rows[row_index].nil?
123
+ end
124
+
125
+ result_rows = add_zero_results(result_rows, dimension)
126
+
127
+ {
128
+ :value => values[-1],
129
+ :count => result_rows.inject(0){|t, r| t + r[:count].to_i},
130
+ :volume => result_rows.inject(0){|t, r| t + r[:volume]},
131
+ :volume_compensated => result_rows.inject(0){|t, r| t + r[:volume_compensated]},
132
+ :row_count => result_rows.inject(0){|t, r| t + r[:row_count]},
133
+ :nodes => result_rows.inject(0){|t, r| t + r[:nodes]} + 1,
134
+ :rows => result_rows,
135
+ }
136
+ end
137
+
138
+ def html_export
139
+ index(false)
140
+ render :template => '/transaction_drilldown/html_export', :layout => 'print'
141
+ end
142
+
143
+ def excel_export
144
+ index(false)
145
+ headers['Content-Type'] = "application/vnd.ms-excel"
146
+ headers['Content-Disposition'] = 'attachment; filename="transactions.xml"'
147
+ headers['Cache-Control'] = ''
148
+ render :template => '/transaction_drilldown/excel_export', :layout => false
149
+ end
150
+
151
+ def excel_export_transactions
152
+ params[:search][:list] = '1'
153
+ index(false)
154
+ @transactions = get_transactions(@result)
155
+ headers['Content-Type'] = "application/vnd.ms-excel"
156
+ headers['Content-Disposition'] = 'attachment; filename="transactions.xml"'
157
+ render :template => '/transaction_drilldown/excel_export_transactions', :layout => false
158
+ end
159
+
160
+ private
161
+
162
+ def populate_list(conditions, includes, result, values)
163
+ if result[:rows]
164
+ result[:rows].each do |r|
165
+ populate_list(conditions, includes, r, values + [r[:value]])
166
+ end
167
+ else
168
+ options = {:include => includes + @list_includes, :order => @list_order}
169
+ @search.fields.each do |field|
170
+ field_def = @transaction_fields_map[field.to_sym]
171
+ raise "Field definition missing for: #{field.inspect}" unless field_def
172
+ field_includes = field_def[:include]
173
+ if field_includes
174
+ options[:include] += field_includes.is_a?(Array) ? field_includes : [field_includes]
175
+ end
176
+ end
177
+ options[:include].uniq!
178
+ if @search.last_change_time
179
+ options[:include] << {:assignment => {:order => :last_aircraft_registration_change}} if @search.fields.include? 'aircraft_registration'
180
+ options[:include] << {:assignment => {:order => :last_fuel_request_change}} if @search.fields.include? 'fuel_request'
181
+ end
182
+ result[:transactions] = @target_class.all(options.update(:conditions => list_conditions(conditions, values)))
183
+ end
184
+ end
185
+
186
+ def list_conditions(conditions, values)
187
+ list_conditions_string = conditions[0].dup
188
+ @dimensions.each do |d|
189
+ list_conditions_string << "#{' AND ' unless list_conditions_string.empty?}#{d[:select_expression]} = ?"
190
+ end
191
+ [list_conditions_string, *(conditions[1..-1] + values)]
192
+ end
193
+
194
+ def make_conditions(search_filter)
195
+ includes = []
196
+ if search_filter
197
+ condition_strings = []
198
+ condition_values = []
199
+
200
+ filter_texts = []
201
+ search_filter.each do |field, values|
202
+ dimension_def = @dimension_defs[field]
203
+ raise "Unknown filter field: #{field.inspect}" if dimension_def.nil?
204
+ values = [*values]
205
+ if dimension_def[:interval]
206
+ if dimension_def[:condition_values_method]
207
+ condition_values += dimension_def[:condition_values_method].call(values)
208
+ else
209
+ condition_values += [*values]
210
+ end
211
+ filter_texts << "#{dimension_def[:pretty_name]} #{dimension_def[:label_method] ? dimension_def[:label_method].call(values) : "from #{values[0]} to #{values[1] || values[0]}"}"
212
+ includes << dimension_def[:includes] if dimension_def[:includes]
213
+ condition_strings << dimension_def[:condition_string]
214
+ else
215
+ condition_strings << values.map do |value|
216
+ if dimension_def[:condition_values_method]
217
+ condition_values += dimension_def[:condition_values_method].call(value)
218
+ else
219
+ condition_values << value
220
+ end
221
+ filter_texts << "#{dimension_def[:pretty_name]} #{dimension_def[:label_method] ? dimension_def[:label_method].call(value) : value}"
222
+ includes << dimension_def[:includes] if dimension_def[:includes]
223
+ dimension_def[:condition_string]
224
+ end.join(' OR ')
225
+ end
226
+ end
227
+ filter_text = filter_texts.join(' and ')
228
+ conditions = [condition_strings.map{|c|"(#{c})"}.join(" AND "), *condition_values]
229
+ includes.uniq!
230
+ else
231
+ filter_text = nil
232
+ conditions = nil
233
+ end
234
+ return conditions, filter_text, includes
235
+ end
236
+
237
+ def legal_values_for(field, preserve_filter = false)
238
+ lambda do |search|
239
+ my_filter = search.filter.dup
240
+ my_filter.delete(field.to_s) unless preserve_filter
241
+ conditions, t, includes = make_conditions(my_filter)
242
+ dimension = @dimension_defs[field.to_s]
243
+ if dimension[:includes]
244
+ if dimension[:includes].is_a?(Array)
245
+ includes += dimension[:includes]
246
+ else
247
+ includes << dimension[:includes]
248
+ end
249
+ includes.uniq!
250
+ end
251
+ rows = @target_class.find(
252
+ :all,
253
+ :select => "DISTINCT(#{dimension[:select_expression]}) AS value",
254
+ :conditions => conditions,
255
+ :joins => make_join(@target_class.name.underscore.to_sym, includes),
256
+ :order => 'value'
257
+ )
258
+ if search.filter[field.to_s]
259
+ search.filter[field.to_s].each do |selected_value|
260
+ unless rows.find{|r| r[:value] == selected_value}
261
+ rows << {:value => selected_value}
262
+ end
263
+ end
264
+ rows.sort_by{|r| r[:value]}
265
+ end
266
+ values = rows.map{|r| [dimension[:label_method] && dimension[:label_method].call(r[:value]) || r[:value], r[:value]]}
267
+ values
268
+ end
269
+ end
270
+
271
+ def dimension(name, select_expression, options = {})
272
+ includes = options.delete(:includes)
273
+ interval = options.delete(:interval)
274
+ label_method = options.delete(:label_method)
275
+ legal_values = options.delete(:legal_values) || legal_values_for(name)
276
+
277
+ raise "Unknown options: #{options.keys.inspect}" unless options.empty?
278
+
279
+ @dimension_defs[name.to_s] = {
280
+ :select_expression => select_expression,
281
+ :condition_string => interval ? "#{select_expression} BETWEEN ? AND ?" : "(#{select_expression}) = ?",
282
+ :condition_values_method => interval ? lambda { |values| [values[0], values[1] || values[0]]} : nil,
283
+ :pretty_name => t(name),
284
+ :url_param_name => name.to_s,
285
+ :legal_values => legal_values,
286
+ :label_method => label_method,
287
+ :includes => includes,
288
+ :interval => interval,
289
+ }
290
+ end
291
+
292
+ def get_transactions(tree)
293
+ return tree[:transactions] if tree[:transactions]
294
+ tree[:rows].map{|r| get_transactions(r)}.flatten
295
+ end
296
+
297
+ def make_join(model, include)
298
+ case include
299
+ when Array
300
+ include.map{|i| make_join(model, i)}.join(' ')
301
+ when Hash
302
+ sql = ''
303
+ include.each do |parent, child|
304
+ parent_table = parent.to_s.pluralize
305
+ sql << make_join(model, parent) + ' '
306
+ sql << make_join(parent, child)
307
+ end
308
+ sql
309
+ when Symbol
310
+ ass = model.to_s.camelize.constantize.reflect_on_association include
311
+ include_table = ass.table_name
312
+ "LEFT JOIN #{include_table} ON #{include_table}.id = #{model.to_s.pluralize}.#{include}_id"
313
+ else
314
+ raise 'Unknown join class: #{include.inspect}'
315
+ end
316
+ end
317
+
318
+ end
@@ -0,0 +1,98 @@
1
+ module SimpleDrilldown
2
+ class Search
3
+ module DisplayType
4
+ BAR = 'BAR'
5
+ LINE = 'LINE'
6
+ NONE = 'NONE'
7
+ PIE = 'PIE'
8
+ end
9
+
10
+ module SelectValue
11
+ COUNT = 'COUNT'
12
+ VOLUME = 'VOLUME'
13
+ end
14
+
15
+ attr_reader :dimensions
16
+ attr_reader :display_type
17
+ attr_reader :fields
18
+ attr_reader :filter
19
+ attr_accessor :list
20
+ attr_reader :last_change_time
21
+ attr_reader :order_by_value
22
+ attr_reader :select_value
23
+ attr_reader :title
24
+ attr_reader :default_fields
25
+
26
+ def initialize(attributes_or_search, default_fields = nil)
27
+ if attributes_or_search.is_a? Search
28
+ s = attributes_or_search
29
+ @dimensions = s.dimensions.dup
30
+ @display_type = s.display_type.dup
31
+ @fields = s.fields.dup
32
+ @filter = s.filter.dup
33
+ @list = s.list
34
+ @last_change_time = s.last_change_time
35
+ @order_by_value = s.order_by_value
36
+ @select_value = s.select_value.dup
37
+ @title = s.title
38
+ @default_fields = s.default_fields
39
+ else
40
+ attributes = attributes_or_search
41
+ @default_fields = default_fields
42
+ @dimensions = attributes && attributes[:dimensions] || []
43
+ @dimensions.delete_if { |d| d.empty? }
44
+ @filter = attributes && attributes[:filter] ? attributes[:filter] : {}
45
+ @filter.each { |k, v| v.delete('') }
46
+ @filter.delete_if { |k, v| v.empty? }
47
+ @display_type = attributes && attributes[:display_type] ? attributes[:display_type] : DisplayType::NONE
48
+ if @dimensions.size >= 2 && @display_type == DisplayType::PIE
49
+ @display_type = DisplayType::BAR
50
+ end
51
+
52
+ @order_by_value = attributes && (attributes[:order_by_value] == '1')
53
+ @select_value = attributes && attributes[:select_value] && attributes[:select_value].size > 0 ? attributes[:select_value] : SelectValue::COUNT
54
+ @list = attributes && attributes[:list] && attributes[:list] == '1' || false
55
+ @last_change_time = attributes && attributes[:last_change_time] && attributes[:last_change_time] == '1' || false
56
+ if (attributes && attributes[:fields])
57
+ if attributes[:fields].is_a?(Array)
58
+ @fields = attributes[:fields]
59
+ else
60
+ @fields = attributes[:fields].select { |k, v| v == '1' }.map { |k, v| k }
61
+ end
62
+ else
63
+ @fields = @default_fields
64
+ end
65
+ @title = attributes[:title] if attributes && attributes[:title] && attributes[:title].size > 0
66
+ end
67
+ end
68
+
69
+ def url_options
70
+ o = {
71
+ :search => {
72
+ :title => title,
73
+ :list => list ? '1' : '0',
74
+ :last_change_time => last_change_time ? '1' : '0',
75
+ :filter => filter,
76
+ :dimensions => dimensions,
77
+ :display_type => display_type,
78
+ }
79
+ }
80
+ o[:search][:fields] = fields unless fields == @default_fields
81
+ o
82
+ end
83
+
84
+ # Used for DOM id
85
+ def id
86
+ 'SEARCH'
87
+ end
88
+
89
+ def drill_down(dimensions, *values)
90
+ raise "Too many values" if values.size > self.dimensions.size
91
+ s = Search.new(self)
92
+ values.each_with_index { |v, i| s.filter[dimensions[i][:url_param_name]] = [v] }
93
+ values.size.times { s.dimensions.shift }
94
+ s
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,66 @@
1
+ module TransactionDrillownHelper
2
+ def value_label(dimension_index, value)
3
+ return nil if @dimensions[dimension_index].nil?
4
+ h(@dimensions[dimension_index][:label_method] ?
5
+ @dimensions[dimension_index][:label_method].call(value) :
6
+ value)
7
+ end
8
+
9
+ def caption
10
+ result = @search.title ? @search.title : "Transaction #{l(@search.select_value.downcase)}" + (@dimensions && @dimensions[0] && @dimensions[0][:pretty_name] ? " by #{@dimensions[0][:pretty_name]}" : "")
11
+ result.gsub('$date', [*@search.filter[:calendar_date]].uniq.join(' - '))
12
+ end
13
+
14
+ def subcaption
15
+ @search.title ? '' : @filter_text && @filter_text.size > 0 ? "for #{@filter_text}" : ""
16
+ end
17
+
18
+ def summary_row(result, dimension = 0, headers = [], new_row = true)
19
+ html = render(:partial => '/transaction_drilldown/summary_row', :locals => {:result => result, :new_row => new_row, :dimension => dimension, :headers => headers, :with_results => !result[:rows]})
20
+ if result[:rows]
21
+ sub_headers = headers + [{:value => result[:value], :display_row_count => result[:nodes] + result[:row_count] * (@search.list ? 1 : 0)}]
22
+ significant_rows = result[:rows].select{|r| r[:row_count] != 0}
23
+ significant_rows.each_with_index do |r, i|
24
+ html << summary_row(r, dimension + 1, sub_headers, i > 0)
25
+ end
26
+ elsif @search.list
27
+ html << render(:partial => '/transaction_drilldown/transaction_list', :locals => {:result => result})
28
+ end
29
+ if dimension < @dimensions.size
30
+ html << render(:partial => '/transaction_drilldown/summary_total_row', :locals => {:result => result, :headers => headers.dup, :dimension => dimension})
31
+ end
32
+
33
+ return html
34
+ end
35
+
36
+ def excel_summary_row(result, dimension, headers)
37
+ xml = ''
38
+ if result[:rows]
39
+ significant_rows = result[:rows].select{|r| r[:row_count] != 0}
40
+ significant_rows.each_with_index do |r, i|
41
+ if i == 0
42
+ if dimension == 0
43
+ sub_headers = headers
44
+ else
45
+ sub_headers = headers + [{:value => result[:value], :display_row_count => result[:nodes] + result[:row_count] * (@search.list ? 1 : 0)}]
46
+ end
47
+ else
48
+ sub_headers = [] # [{:value => result[:value], :row_count => result[:row_count]}]
49
+ end
50
+ xml << excel_summary_row(r, dimension + 1, sub_headers)
51
+ end
52
+ else
53
+ xml << render(:partial => '/transaction_drilldown/excel_summary_row', :locals => {:result => result, :headers => headers.dup, :dimension => dimension})
54
+
55
+ if @search.list
56
+ xml << render(:partial => '/transaction_drilldown/excel_transaction_list', :locals => {:result => result})
57
+ end
58
+ end
59
+
60
+ if dimension < @dimensions.size
61
+ xml << render(:partial => '/transaction_drilldown/excel_summary_total_row', :locals => {:result => result, :headers => headers.dup, :dimension => dimension})
62
+ end
63
+ xml
64
+ end
65
+
66
+ end
@@ -0,0 +1,3 @@
1
+ module SimpleDrilldown
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,7 @@
1
+ require "simple_drilldown/version"
2
+ require 'simple_drilldown/search'
3
+ require 'simple_drilldown/drilldown_controller'
4
+
5
+ module SimpleDrilldown
6
+ # Your code goes here...
7
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'simple_drilldown/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'simple_drilldown'
8
+ spec.version = SimpleDrilldown::VERSION
9
+ spec.authors = ['Uwe Kubosch']
10
+ spec.email = %w(uwe@kubosch.no)
11
+ spec.summary = %q{Simple data warehouse and drilldown.}
12
+ spec.description = %q{simple_drilldown offers a simple way to define axis to filter and group records for analysis.}
13
+ spec.homepage = 'http://github.com/donv/simple_drilldown'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = %w(lib)
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.3'
22
+ spec.add_development_dependency 'rake'
23
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require 'shoulda'
12
+
13
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
14
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
15
+ require 'simple_drilldown'
16
+
17
+ class Test::Unit::TestCase
18
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestSimpleDrilldown < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_drilldown
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Uwe Kubosch
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-07-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: simple_drilldown offers a simple way to define axis to filter and group
42
+ records for analysis.
43
+ email:
44
+ - uwe@kubosch.no
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - .document
50
+ - .gitignore
51
+ - Gemfile
52
+ - Gemfile.lock
53
+ - LICENSE.txt
54
+ - README.md
55
+ - README.rdoc
56
+ - Rakefile
57
+ - lib/sample_drilldown_controller.rb
58
+ - lib/simple_drilldown.rb
59
+ - lib/simple_drilldown/drilldown_controller.rb
60
+ - lib/simple_drilldown/search.rb
61
+ - lib/simple_drilldown/simple_drilldown_helper.rb
62
+ - lib/simple_drilldown/version.rb
63
+ - simple_drilldown.gemspec
64
+ - test/helper.rb
65
+ - test/test_simple_drilldown.rb
66
+ homepage: http://github.com/donv/simple_drilldown
67
+ licenses:
68
+ - MIT
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 2.0.3
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: Simple data warehouse and drilldown.
90
+ test_files:
91
+ - test/helper.rb
92
+ - test/test_simple_drilldown.rb