simple_drilldown 0.0.3 → 0.1.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.
- checksums.yaml +5 -5
- data/{LICENSE.txt → MIT-LICENSE} +1 -3
- data/README.md +15 -16
- data/Rakefile +32 -0
- data/app/assets/config/simple_drilldown_manifest.js +1 -0
- data/app/assets/stylesheets/simple_drilldown/application.css +121 -0
- data/app/controllers/simple_drilldown/application_controller.rb +7 -0
- data/app/helpers/simple_drilldown/application_helper.rb +6 -0
- data/app/jobs/simple_drilldown/application_job.rb +6 -0
- data/app/mailers/simple_drilldown/application_mailer.rb +8 -0
- data/app/models/simple_drilldown/application_record.rb +7 -0
- data/app/views/drilldown/_chart.html.erb +57 -0
- data/app/views/drilldown/_excel_record_list.builder +7 -0
- data/app/views/drilldown/_excel_row.builder +24 -0
- data/app/views/drilldown/_excel_row_header.builder +10 -0
- data/app/views/drilldown/_excel_styles.builder +69 -0
- data/app/views/drilldown/_excel_summary_row.builder +16 -0
- data/app/views/drilldown/_excel_summary_total_row.builder +14 -0
- data/app/views/drilldown/_field.html.erb +39 -0
- data/app/views/drilldown/_fields.html.erb +12 -0
- data/app/views/drilldown/_filter.html.erb +20 -0
- data/app/views/drilldown/_record_list.html.erb +12 -0
- data/app/views/drilldown/_row.html.erb +13 -0
- data/app/views/drilldown/_row_header.html.erb +8 -0
- data/app/views/drilldown/_summary_row.html.erb +18 -0
- data/app/views/drilldown/_summary_table.html.erb +12 -0
- data/app/views/drilldown/_summary_total_row.html.erb +4 -0
- data/app/views/drilldown/_tab_buttons.html.erb +5 -0
- data/app/views/drilldown/data_0.builder +10 -0
- data/app/views/drilldown/data_1.builder +11 -0
- data/app/views/drilldown/data_2.builder +37 -0
- data/app/views/drilldown/data_3.builder +37 -0
- data/app/views/drilldown/excel_export.builder +42 -0
- data/app/views/drilldown/excel_export_transactions.builder +90 -0
- data/app/views/drilldown/html_export.html.erb +6 -0
- data/app/views/drilldown/index.html.erb +107 -0
- data/app/views/drilldown/print.html.erb +18 -0
- data/app/views/layouts/simple_drilldown/application.html.erb +15 -0
- data/config/locales/en.yml +23 -0
- data/config/locales/nb.yml +20 -0
- data/config/routes.rb +4 -0
- data/lib/generators/drilldown_controller/USAGE +8 -0
- data/lib/generators/drilldown_controller/drilldown_controller_generator.rb +8 -0
- data/lib/generators/drilldown_controller/templates/drilldown_controller.rb.erb +41 -0
- data/lib/simple_drilldown.rb +3 -3
- data/lib/simple_drilldown/drilldown_controller.rb +389 -274
- data/lib/simple_drilldown/drilldown_helper.rb +68 -0
- data/lib/simple_drilldown/engine.rb +11 -0
- data/lib/simple_drilldown/search.rb +12 -1
- data/lib/simple_drilldown/version.rb +1 -1
- data/lib/tasks/simple_drilldown_tasks.rake +5 -0
- metadata +85 -34
- data/.document +0 -5
- data/.gitignore +0 -22
- data/Gemfile +0 -3
- data/Gemfile.lock +0 -89
- data/README.rdoc +0 -19
- data/lib/sample_drilldown_controller.rb +0 -95
- data/lib/simple_drilldown/simple_drilldown_helper.rb +0 -66
- data/simple_drilldown.gemspec +0 -24
- data/test/helper.rb +0 -18
- data/test/test_simple_drilldown.rb +0 -7
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
|
2
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
3
|
+
|
|
4
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
|
5
|
+
<%= render 'layouts/headers' %>
|
|
6
|
+
<%= stylesheet_link_tag 'drilldown' %>
|
|
7
|
+
<body>
|
|
8
|
+
<style type="text/css">
|
|
9
|
+
@media print {#print_link {display: none}}
|
|
10
|
+
</style>
|
|
11
|
+
<div class="container">
|
|
12
|
+
<div id="print_link" class="pull-right">
|
|
13
|
+
<%= link_to '<i class="btn btn-primary glyphicon glyphicon-print"> Print</i>'.html_safe, '#', onclick: 'window.print();' %>
|
|
14
|
+
</div>
|
|
15
|
+
<%= yield %>
|
|
16
|
+
</div>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Simple drilldown</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= stylesheet_link_tag "simple_drilldown/application", media: "all" %>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
|
|
12
|
+
<%= yield %>
|
|
13
|
+
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
en:
|
|
2
|
+
all: All
|
|
3
|
+
bar: Bar
|
|
4
|
+
chart_type: Chart type
|
|
5
|
+
count: Count
|
|
6
|
+
fields: Fields
|
|
7
|
+
filter: Filter
|
|
8
|
+
from_date: From date
|
|
9
|
+
group_by: Group by
|
|
10
|
+
line: Line
|
|
11
|
+
list: List
|
|
12
|
+
none: None
|
|
13
|
+
order_by_value: Order by value
|
|
14
|
+
pie: Pie
|
|
15
|
+
reset: Reset
|
|
16
|
+
search: Search
|
|
17
|
+
show: Show
|
|
18
|
+
then_by: then by
|
|
19
|
+
title: Title
|
|
20
|
+
to_date: To date
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
nb:
|
|
2
|
+
all: Alle
|
|
3
|
+
bar: Stolper
|
|
4
|
+
chart_type: Graftype
|
|
5
|
+
count: Antall
|
|
6
|
+
fields: Felt
|
|
7
|
+
filter: Filter
|
|
8
|
+
from_date: Fra dato
|
|
9
|
+
group_by: Grupper på
|
|
10
|
+
line: Linje
|
|
11
|
+
list: Liste
|
|
12
|
+
none: Ingen
|
|
13
|
+
order_by_value: Sorter på verdi
|
|
14
|
+
pie: Pai
|
|
15
|
+
reset: Nullstill
|
|
16
|
+
search: Søk
|
|
17
|
+
show: Vis
|
|
18
|
+
then_by: så på
|
|
19
|
+
title: Tittel
|
|
20
|
+
to_date: Til dato
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class DrilldownControllerGenerator < Rails::Generators::NamedBase
|
|
2
|
+
source_root File.expand_path('templates', __dir__)
|
|
3
|
+
|
|
4
|
+
def copy_drilldown_controller_file
|
|
5
|
+
template "drilldown_controller.rb.erb", "app/controllers/#{file_name}_drilldown_controller.rb"
|
|
6
|
+
route "resources(:#{singular_name}_drilldown, only: :index){collection{get :excel_export;get :html_export}}"
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %>DrilldownController < DrilldownController
|
|
4
|
+
# What fields should be displayed as default when listing actual <%= class_name %> records.
|
|
5
|
+
default_fields %w[created_at updated_at] # TODO(uwe): Read fields from schema?
|
|
6
|
+
|
|
7
|
+
# The main focus of the drilldown
|
|
8
|
+
target_class <%= class_name %>
|
|
9
|
+
|
|
10
|
+
# How should we count the reords?
|
|
11
|
+
select 'count(*) as count'
|
|
12
|
+
|
|
13
|
+
# When listing records, what relations should be included for optimization?
|
|
14
|
+
# list_includes :user, :comments # TODO(uwe): Read relations from schema?
|
|
15
|
+
|
|
16
|
+
# In what order should records be listed?
|
|
17
|
+
list_order '<%= plural_name %>.created_at'
|
|
18
|
+
|
|
19
|
+
# Field definitions when listing records
|
|
20
|
+
field :created_at
|
|
21
|
+
field :updated_at
|
|
22
|
+
|
|
23
|
+
# The "attr_method" option transforms the value from the database to a
|
|
24
|
+
# readable form.
|
|
25
|
+
# field :user, attr_method: ->(post) { post.user.name }
|
|
26
|
+
# field :body, attr_method: ->(post) { post.body[0..32] }
|
|
27
|
+
# field :comments, attr_method: ->(post) { post.comments.count }
|
|
28
|
+
|
|
29
|
+
dimension :calendar_date, 'DATE(<%= plural_name %>.created_at)', interval: true
|
|
30
|
+
dimension :day_of_month, "date_part('day', <%= plural_name %>.created_at)"
|
|
31
|
+
dimension :day_of_week, "CASE WHEN date_part('dow', <%= plural_name %>.created_at) = 0 THEN 7 ELSE date_part('dow', <%= plural_name %>.created_at) END",
|
|
32
|
+
label_method: ->(day_no) { Date::DAYNAMES[day_no.to_i % 7] }
|
|
33
|
+
dimension :hour_of_day, "date_part('hour', <%= plural_name %>.created_at)"
|
|
34
|
+
dimension :month, "date_part('month', <%= plural_name %>.created_at)",
|
|
35
|
+
label_method: ->(month_no) { Date::MONTHNAMES[month_no.to_i] }
|
|
36
|
+
dimension :week, "date_part('week', <%= plural_name %>.created_at)"
|
|
37
|
+
dimension :year, "date_part('year', <%= plural_name %>.created_at)"
|
|
38
|
+
|
|
39
|
+
# dimension :comments, 'SELECT count(*) FROM comments c WHERE c.<%= singular_name %>_id = <%= plural_name %>.id'
|
|
40
|
+
# dimension :user, 'users.name', includes: :user
|
|
41
|
+
end
|
data/lib/simple_drilldown.rb
CHANGED
|
@@ -1,318 +1,433 @@
|
|
|
1
|
-
|
|
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
|
|
1
|
+
# frozen_string_literal: true
|
|
11
2
|
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
3
|
+
require 'simple_drilldown/drilldown_helper'
|
|
4
|
+
require 'simple_drilldown/search'
|
|
20
5
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@search = Search.new(params[:search], @default_fields)
|
|
6
|
+
module SimpleDrilldown
|
|
7
|
+
class DrilldownController < ::ApplicationController
|
|
8
|
+
helper DrilldownHelper
|
|
25
9
|
|
|
26
|
-
|
|
27
|
-
@transaction_fields_map = @fields
|
|
10
|
+
LIST_LIMIT = 10_000
|
|
28
11
|
|
|
29
|
-
|
|
30
|
-
|
|
12
|
+
class << self
|
|
13
|
+
def base_condition(base_condition)
|
|
14
|
+
@@base_condition = base_condition
|
|
15
|
+
end
|
|
31
16
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
17
|
+
def base_includes(base_includes)
|
|
18
|
+
@@base_includes = base_includes
|
|
19
|
+
end
|
|
42
20
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
21
|
+
def base_group(base_group)
|
|
22
|
+
@@base_group = base_group
|
|
23
|
+
end
|
|
67
24
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
end
|
|
25
|
+
def default_fields(fields)
|
|
26
|
+
@@default_fields = fields
|
|
27
|
+
end
|
|
72
28
|
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
29
|
+
def target_class(target_class)
|
|
30
|
+
@@target_class = target_class
|
|
93
31
|
end
|
|
94
|
-
result_rows = result_rows.sort_by { |r| legal_values.index(r[:value]) }
|
|
95
|
-
end
|
|
96
|
-
result_rows
|
|
97
|
-
end
|
|
98
32
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
33
|
+
def select(select)
|
|
34
|
+
@@select = select
|
|
35
|
+
end
|
|
115
36
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
37
|
+
def list_includes(list_includes)
|
|
38
|
+
@@list_includes = list_includes
|
|
39
|
+
end
|
|
124
40
|
|
|
125
|
-
|
|
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
|
|
41
|
+
def list_order(list_order)
|
|
42
|
+
@@list_order = list_order
|
|
43
|
+
end
|
|
137
44
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
45
|
+
def field(name, **options)
|
|
46
|
+
@@fields ||= {}
|
|
47
|
+
@@fields[name] = options
|
|
48
|
+
end
|
|
142
49
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
end
|
|
50
|
+
def dimension(name, select_expression = name.to_s, options = {})
|
|
51
|
+
includes = options.delete(:includes)
|
|
52
|
+
interval = options.delete(:interval)
|
|
53
|
+
label_method = options.delete(:label_method) || ->(f) { f.to_s }
|
|
54
|
+
legal_values = options.delete(:legal_values) || legal_values_for(name)
|
|
55
|
+
reverse = options.delete(:reverse)
|
|
150
56
|
|
|
151
|
-
|
|
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
|
|
57
|
+
raise "Unknown options: #{options.keys.inspect}" unless options.empty?
|
|
159
58
|
|
|
160
|
-
|
|
59
|
+
@@dimension_defs ||= Concurrent::Hash.new
|
|
161
60
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
61
|
+
@@dimension_defs[name.to_s] = {
|
|
62
|
+
select_expression: select_expression,
|
|
63
|
+
pretty_name: I18n.t(name),
|
|
64
|
+
url_param_name: name.to_s,
|
|
65
|
+
legal_values: legal_values,
|
|
66
|
+
label_method: label_method,
|
|
67
|
+
reverse: reverse,
|
|
68
|
+
includes: includes,
|
|
69
|
+
interval: interval
|
|
70
|
+
}
|
|
166
71
|
end
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
72
|
+
|
|
73
|
+
def legal_values_for(field, preserve_filter = false)
|
|
74
|
+
lambda do |search|
|
|
75
|
+
my_filter = search.filter.dup
|
|
76
|
+
my_filter.delete(field.to_s) unless preserve_filter
|
|
77
|
+
conditions, t, includes = make_conditions(my_filter)
|
|
78
|
+
dimension = @@dimension_defs[field.to_s]
|
|
79
|
+
if dimension[:includes]
|
|
80
|
+
if dimension[:includes].is_a?(Array)
|
|
81
|
+
includes += dimension[:includes]
|
|
82
|
+
else
|
|
83
|
+
includes << dimension[:includes]
|
|
84
|
+
end
|
|
85
|
+
includes.uniq!
|
|
86
|
+
end
|
|
87
|
+
rows = @@target_class.unscoped.where(@@base_condition)
|
|
88
|
+
.select("#{dimension[:select_expression]} AS value")
|
|
89
|
+
.where(conditions)
|
|
90
|
+
.joins(make_join([], @@target_class.name.underscore.to_sym, includes))
|
|
91
|
+
.order('value')
|
|
92
|
+
.group('value').all.to_a
|
|
93
|
+
search.filter[field.to_s]&.each do |selected_value|
|
|
94
|
+
unless rows.find { |r| dimension[:label_method].call(r[:value]) == selected_value }
|
|
95
|
+
rows << { value: selected_value }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
values = rows.map { |r| [dimension[:label_method]&.call(r[:value]) || r[:value], r[:value]] }.sort_by { |a| a[0].upcase }
|
|
99
|
+
values.reverse! if dimension[:reverse]
|
|
100
|
+
values
|
|
175
101
|
end
|
|
176
102
|
end
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
103
|
+
|
|
104
|
+
def make_conditions(search_filter)
|
|
105
|
+
includes = @@base_includes.dup
|
|
106
|
+
if search_filter
|
|
107
|
+
condition_strings = []
|
|
108
|
+
condition_values = []
|
|
109
|
+
|
|
110
|
+
filter_texts = []
|
|
111
|
+
search_filter.each do |field, values|
|
|
112
|
+
dimension_def = @@dimension_defs[field]
|
|
113
|
+
raise "Unknown filter field: #{field.inspect}" if dimension_def.nil?
|
|
114
|
+
|
|
115
|
+
values = [*values]
|
|
116
|
+
if dimension_def[:interval]
|
|
117
|
+
values *= 2 if values.size == 1
|
|
118
|
+
if values.size != 2
|
|
119
|
+
raise "Need 2 values for interval filter: #{values.inspect}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if !values[0].blank? && !values[1].blank?
|
|
123
|
+
condition_strings << "#{dimension_def[:select_expression]} BETWEEN ? AND ?"
|
|
124
|
+
condition_values += values
|
|
125
|
+
filter_texts << "#{dimension_def[:pretty_name]} #{dimension_def[:label_method] ? dimension_def[:label_method].call(values) : "from #{values[0]} to #{values[1]}"}"
|
|
126
|
+
elsif !values[0].blank?
|
|
127
|
+
condition_strings << "#{dimension_def[:select_expression]} >= ?"
|
|
128
|
+
condition_values < values[0]
|
|
129
|
+
filter_texts << "#{dimension_def[:pretty_name]} #{dimension_def[:label_method] ? dimension_def[:label_method].call(values) : "from #{values[0]}"}"
|
|
130
|
+
elsif !values[1].blank?
|
|
131
|
+
condition_strings << "#{dimension_def[:select_expression]} <= ?"
|
|
132
|
+
condition_values < values[1]
|
|
133
|
+
filter_texts << "#{dimension_def[:pretty_name]} #{dimension_def[:label_method] ? dimension_def[:label_method].call(values) : "to #{values[1]}"}"
|
|
134
|
+
end
|
|
135
|
+
includes << dimension_def[:includes] if dimension_def[:includes]
|
|
136
|
+
else
|
|
137
|
+
condition_strings << values.map do |value|
|
|
138
|
+
if dimension_def[:condition_values_method]
|
|
139
|
+
condition_values += dimension_def[:condition_values_method].call(value)
|
|
140
|
+
else
|
|
141
|
+
condition_values << value
|
|
142
|
+
end
|
|
143
|
+
filter_texts << "#{dimension_def[:pretty_name]} #{dimension_def[:label_method] ? dimension_def[:label_method].call(value) : value}"
|
|
144
|
+
includes << dimension_def[:includes] if dimension_def[:includes]
|
|
145
|
+
"(#{dimension_def[:select_expression]}) = ?"
|
|
146
|
+
end.join(' OR ')
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
filter_text = filter_texts.join(' and ')
|
|
150
|
+
conditions = [condition_strings.map { |c| "(#{c})" }.join(' AND '), *condition_values]
|
|
151
|
+
includes.uniq!
|
|
152
|
+
else
|
|
153
|
+
filter_text = nil
|
|
154
|
+
conditions = nil
|
|
155
|
+
end
|
|
156
|
+
[conditions, filter_text, includes]
|
|
181
157
|
end
|
|
182
|
-
result[:transactions] = @target_class.all(options.update(:conditions => list_conditions(conditions, values)))
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
158
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
159
|
+
def make_join(joins, model, include, model_class = model.to_s.camelize.constantize)
|
|
160
|
+
case include
|
|
161
|
+
when Array
|
|
162
|
+
include.map { |i| make_join(joins, model, i) }.join(' ')
|
|
163
|
+
when Hash
|
|
164
|
+
sql = ''
|
|
165
|
+
include.each do |parent, child|
|
|
166
|
+
sql << make_join(joins, model, parent) + ' '
|
|
167
|
+
ass = model.to_s.camelize.constantize.reflect_on_association parent
|
|
168
|
+
sql << make_join(joins, parent, child, ass.class_name.constantize)
|
|
169
|
+
end
|
|
170
|
+
sql
|
|
171
|
+
when Symbol
|
|
172
|
+
return '' if joins.include?(include)
|
|
173
|
+
|
|
174
|
+
joins << include
|
|
175
|
+
ass = model_class.reflect_on_association include
|
|
176
|
+
raise "Unknown association: #{model} => #{include}" unless ass
|
|
193
177
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
178
|
+
model_table = model.to_s.pluralize
|
|
179
|
+
include_table = ass.table_name
|
|
180
|
+
include_alias = include.to_s.pluralize
|
|
181
|
+
case ass.macro
|
|
182
|
+
when :belongs_to
|
|
183
|
+
"LEFT JOIN #{include_table} #{include_alias} ON #{include_alias}.id = #{model_table}.#{include}_id"
|
|
184
|
+
when :has_one, :has_many
|
|
185
|
+
fk_col = ass.options[:foreign_key] || "#{model}_id"
|
|
186
|
+
sql = "LEFT JOIN #{include_table} #{include_alias} ON #{include_alias}.#{fk_col} = #{model_table}.id"
|
|
187
|
+
if (ass_order = ass.options[:order].try(:to_s))
|
|
188
|
+
ass_order.sub!(/ DESC\s*$/i, '')
|
|
189
|
+
ass_order_prefixed = ass_order.dup
|
|
190
|
+
ActiveRecord::Base.connection.columns(include_table).map(&:name).each do |cname|
|
|
191
|
+
ass_order_prefixed.gsub!(/\b#{cname}\b/, "#{include_alias}.#{cname}")
|
|
192
|
+
end
|
|
193
|
+
sql << " AND #{ass_order_prefixed} = (SELECT MIN(#{ass_order}) FROM #{include_table} t2 WHERE t2.#{fk_col} = #{model_table}.id #{
|
|
194
|
+
if ass.klass.paranoid?
|
|
195
|
+
'AND t2.deleted_at IS NULL'
|
|
196
|
+
end})"
|
|
197
|
+
end
|
|
198
|
+
sql
|
|
208
199
|
else
|
|
209
|
-
|
|
200
|
+
raise "Unknown association type: #{ass.macro}"
|
|
210
201
|
end
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
202
|
+
when String
|
|
203
|
+
include
|
|
204
|
+
when nil
|
|
205
|
+
''
|
|
214
206
|
else
|
|
215
|
-
|
|
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 ')
|
|
207
|
+
raise "Unknown join class: #{include.inspect}"
|
|
225
208
|
end
|
|
226
209
|
end
|
|
227
|
-
|
|
228
|
-
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def initialize
|
|
213
|
+
super()
|
|
214
|
+
@fields = @@fields
|
|
215
|
+
@default_fields = @@default_fields
|
|
216
|
+
@default_select_value = SimpleDrilldown::Search::SelectValue::COUNT
|
|
217
|
+
@target_class = @@target_class
|
|
218
|
+
@select = @@select
|
|
219
|
+
@@base_condition = '1 = 1' unless defined?(@@base_condition)
|
|
220
|
+
@base_condition = @@base_condition
|
|
221
|
+
@@base_includes = [] unless defined?(@@base_includes)
|
|
222
|
+
@base_includes = @@base_includes
|
|
223
|
+
@base_group = defined?(@@base_group) ? @@base_group : []
|
|
224
|
+
@@list_includes = [] unless defined?(@@list_includes)
|
|
225
|
+
@list_includes = @@list_includes
|
|
226
|
+
@list_order = @@list_order
|
|
227
|
+
@dimension_defs = @@dimension_defs
|
|
228
|
+
@summary_fields = []
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# ?dimension[0]=supplier&dimension[1]=transaction_type&
|
|
232
|
+
# filter[year]=2009&filter[supplier][0]=Shell&filter[supplier][1]=Statoil
|
|
233
|
+
def index(do_render = true)
|
|
234
|
+
@search = Search.new(params[:search]&.to_unsafe_h, @default_fields, @default_select_value)
|
|
235
|
+
|
|
236
|
+
@transaction_fields = (@search.fields + (@fields.keys.map(&:to_s) - @search.fields))
|
|
237
|
+
@transaction_fields_map = @fields
|
|
238
|
+
|
|
239
|
+
select = @select.dup
|
|
240
|
+
includes = @base_includes.dup
|
|
241
|
+
|
|
242
|
+
@dimensions = []
|
|
243
|
+
select << ", 'All' as value0"
|
|
244
|
+
@dimensions += @search.dimensions.map do |dn|
|
|
245
|
+
if @dimension_defs[dn].nil?
|
|
246
|
+
raise "Unknown distribution field: #{@search.dimensions.inspect}"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
@dimension_defs[dn]
|
|
250
|
+
end
|
|
251
|
+
@dimensions.each_with_index do |d, i|
|
|
252
|
+
select << ", #{d[:select_expression]} as value#{i + 1}"
|
|
253
|
+
includes << d[:includes] if d[:includes]
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
conditions, @filter_text, filter_includes = self.class.make_conditions(@search.filter)
|
|
257
|
+
includes += filter_includes
|
|
229
258
|
includes.uniq!
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
259
|
+
if @search.order_by_value && @dimensions.size <= 1
|
|
260
|
+
order = 'count DESC'
|
|
261
|
+
else
|
|
262
|
+
order = @dimensions.map { |d| d[:select_expression] }.join(', ')
|
|
263
|
+
order = nil if order.empty?
|
|
264
|
+
end
|
|
265
|
+
group = (@base_group + @dimensions.map { |d| d[:select_expression] }).join(', ')
|
|
266
|
+
group = nil if group.empty?
|
|
267
|
+
|
|
268
|
+
rows = @target_class.unscoped.where(@base_condition).select(select).where(conditions)
|
|
269
|
+
.joins(self.class.make_join([], @target_class.name.underscore.to_sym, includes))
|
|
270
|
+
.group(group)
|
|
271
|
+
.order(order).all.to_a
|
|
272
|
+
|
|
273
|
+
if rows.empty?
|
|
274
|
+
@result = { value: 'All', count: 0, row_count: 0, nodes: 0, rows: [] }
|
|
275
|
+
else
|
|
276
|
+
if do_render && @search.list && rows.inject(0) { |sum, r| sum + r[:count].to_i } > LIST_LIMIT
|
|
277
|
+
@search.list = false
|
|
278
|
+
flash[:notice] = "More than #{LIST_LIMIT} records. List disabled."
|
|
279
|
+
end
|
|
280
|
+
@result = result_from_rows(rows, 0, 0, ['All'])
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
remove_duplicates(@result) unless @base_group.empty?
|
|
284
|
+
|
|
285
|
+
@remaining_dimensions = @dimension_defs.dup.delete_if do |dim_name, _dimension|
|
|
286
|
+
(@search.filter[dim_name] && @search.filter[dim_name].size == 1) ||
|
|
287
|
+
(@dimensions.any? { |d| d[:url_param_name] == dim_name })
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
populate_list(conditions, includes, @result, []) if @search.list
|
|
291
|
+
render template: '/drilldown/index' if do_render
|
|
233
292
|
end
|
|
234
|
-
return conditions, filter_text, includes
|
|
235
|
-
end
|
|
236
293
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
294
|
+
def remove_duplicates(result)
|
|
295
|
+
rows = result[:rows]
|
|
296
|
+
return 0 unless rows
|
|
297
|
+
|
|
298
|
+
removed_rows = 0
|
|
299
|
+
prev_row = nil
|
|
300
|
+
rows.each do |r|
|
|
301
|
+
if prev_row
|
|
302
|
+
if prev_row[:value] == r[:value]
|
|
303
|
+
prev_row[:count] += r[:count]
|
|
304
|
+
prev_row[:row_count] = [prev_row[:row_count], r[:row_count]].max
|
|
305
|
+
prev_row[:nodes] = [prev_row[:nodes], r[:nodes]].max
|
|
306
|
+
prev_row[:rows] += r[:rows] if prev_row[:rows] || r[:rows]
|
|
307
|
+
r[:value] = nil
|
|
308
|
+
removed_rows += r[:nodes]
|
|
309
|
+
end
|
|
248
310
|
end
|
|
249
|
-
|
|
311
|
+
prev_row = r unless r[:value].nil?
|
|
312
|
+
end
|
|
313
|
+
rows.delete_if { |r| r[:value].nil? }
|
|
314
|
+
rows.each do |r|
|
|
315
|
+
removed_child_rows = remove_duplicates(r)
|
|
316
|
+
removed_rows += removed_child_rows
|
|
250
317
|
end
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
318
|
+
result[:row_count] -= removed_rows
|
|
319
|
+
result[:nodes] -= removed_rows
|
|
320
|
+
removed_rows
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Empty summary rows are needed to plot zero points in the charts
|
|
324
|
+
def add_zero_results(result_rows, dimension)
|
|
325
|
+
legal_values = self.class.legal_values_for(@dimensions[dimension][:url_param_name], true).call(@search).map { |lv| lv[1] }
|
|
326
|
+
legal_values.reverse! if @dimensions[dimension][:reverse]
|
|
327
|
+
current_values = result_rows.map { |r| r[:value] }.compact
|
|
328
|
+
empty_values = legal_values - current_values
|
|
329
|
+
|
|
330
|
+
unless empty_values.empty?
|
|
331
|
+
empty_values.each do |v|
|
|
332
|
+
sub_result = {
|
|
333
|
+
value: v,
|
|
334
|
+
count: 0,
|
|
335
|
+
row_count: 0,
|
|
336
|
+
nodes: 0
|
|
337
|
+
}
|
|
338
|
+
if dimension < @dimensions.size - 1
|
|
339
|
+
sub_result[:rows] = add_zero_results([], dimension + 1)
|
|
262
340
|
end
|
|
341
|
+
result_rows << sub_result
|
|
263
342
|
end
|
|
264
|
-
|
|
343
|
+
result_rows = result_rows.sort_by { |r| legal_values.index(r[:value]) }
|
|
265
344
|
end
|
|
266
|
-
|
|
267
|
-
values
|
|
345
|
+
result_rows
|
|
268
346
|
end
|
|
269
|
-
end
|
|
270
347
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
348
|
+
def result_from_rows(rows, row_index, dimension, previous_values)
|
|
349
|
+
row = rows[row_index]
|
|
350
|
+
return nil if row.nil?
|
|
291
351
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
tree[:rows].map { |r| get_transactions(r) }.flatten
|
|
295
|
-
end
|
|
352
|
+
values = (0..dimension).to_a.map { |i| row["value#{i}"] }
|
|
353
|
+
return nil if values != previous_values
|
|
296
354
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
355
|
+
if dimension == @dimensions.size
|
|
356
|
+
return {
|
|
357
|
+
value: values[-1],
|
|
358
|
+
count: row[:count].to_i,
|
|
359
|
+
row_count: 1,
|
|
360
|
+
nodes: @search.list ? 2 : 1
|
|
361
|
+
}
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
result_rows = []
|
|
365
|
+
loop do
|
|
366
|
+
sub_result = result_from_rows(rows, row_index, dimension + 1, values + [rows[row_index]["value#{dimension + 1}"]])
|
|
367
|
+
break if sub_result.nil?
|
|
368
|
+
|
|
369
|
+
result_rows << sub_result
|
|
370
|
+
row_index += sub_result[:row_count]
|
|
371
|
+
break if rows[row_index].nil?
|
|
307
372
|
end
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
373
|
+
|
|
374
|
+
result_rows = add_zero_results(result_rows, dimension)
|
|
375
|
+
|
|
376
|
+
{
|
|
377
|
+
value: values[-1],
|
|
378
|
+
count: result_rows.inject(0) { |t, r| t + r[:count].to_i },
|
|
379
|
+
row_count: result_rows.inject(0) { |t, r| t + r[:row_count] },
|
|
380
|
+
nodes: result_rows.inject(0) { |t, r| t + r[:nodes] } + 1,
|
|
381
|
+
rows: result_rows
|
|
382
|
+
}
|
|
315
383
|
end
|
|
316
|
-
end
|
|
317
384
|
|
|
385
|
+
def html_export
|
|
386
|
+
index(false)
|
|
387
|
+
render template: '/drilldown/html_export', layout: '../drilldown/print'
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def excel_export
|
|
391
|
+
index(false)
|
|
392
|
+
headers['Content-Type'] = 'application/vnd.ms-excel'
|
|
393
|
+
headers['Content-Disposition'] = 'attachment; filename="elections.xls"'
|
|
394
|
+
headers['Cache-Control'] = ''
|
|
395
|
+
render template: '/drilldown/excel_export', layout: false
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
private
|
|
399
|
+
|
|
400
|
+
def populate_list(conditions, includes, result, values)
|
|
401
|
+
if result[:rows]
|
|
402
|
+
result[:rows].each do |r|
|
|
403
|
+
populate_list(conditions, includes, r, values + [r[:value]])
|
|
404
|
+
end
|
|
405
|
+
else
|
|
406
|
+
options = { include: includes + @list_includes, order: @list_order }
|
|
407
|
+
@search.fields.each do |field|
|
|
408
|
+
field_def = @transaction_fields_map[field.to_sym]
|
|
409
|
+
raise "Field definition missing for: #{field.inspect}" unless field_def
|
|
410
|
+
|
|
411
|
+
field_includes = field_def[:include]
|
|
412
|
+
if field_includes
|
|
413
|
+
options[:include] += field_includes.is_a?(Array) ? field_includes : [field_includes]
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
options[:include].uniq!
|
|
417
|
+
|
|
418
|
+
joins = self.class.make_join([], @target_class.name.underscore.to_sym, options.delete(:include))
|
|
419
|
+
result[:transactions] = @target_class.joins(joins).where(@base_condition).where(list_conditions(conditions, values)).includes(options[:include]).order(options[:order]).all
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def list_conditions(conditions, values)
|
|
424
|
+
list_conditions_string = conditions[0].dup
|
|
425
|
+
@dimensions.each do |d|
|
|
426
|
+
list_conditions_string << "#{unless list_conditions_string.empty?
|
|
427
|
+
' AND '
|
|
428
|
+
end}#{d[:select_expression]} = ?"
|
|
429
|
+
end
|
|
430
|
+
[list_conditions_string, *(conditions[1..-1] + values)]
|
|
431
|
+
end
|
|
432
|
+
end
|
|
318
433
|
end
|