simple_drilldown 0.4.0 → 0.6.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +9 -6
- data/app/views/drilldown/_chart.html.erb +23 -3
- data/app/views/drilldown/_field.html.erb +1 -2
- data/app/views/drilldown/_fields.html.erb +1 -1
- data/app/views/drilldown/_filter.html.erb +4 -14
- data/app/views/drilldown/_record_list.html.erb +3 -3
- data/app/views/drilldown/_row.html.erb +9 -9
- data/app/views/drilldown/_row_header.html.erb +1 -1
- data/app/views/drilldown/_summary_table.html.erb +2 -2
- data/app/views/drilldown/excel_export_transactions.builder +1 -1
- data/lib/generators/drilldown_controller/USAGE +1 -0
- data/lib/generators/drilldown_controller/drilldown_controller_generator.rb +2 -1
- data/lib/generators/drilldown_controller/templates/drilldown_controller.rb.erb +14 -5
- data/lib/generators/drilldown_controller/templates/drilldown_controller_test.rb.erb +15 -0
- data/lib/simple_drilldown/{drilldown_controller.rb → controller.rb} +148 -93
- data/lib/simple_drilldown/engine.rb +6 -0
- data/lib/simple_drilldown/{drilldown_helper.rb → helper.rb} +2 -2
- data/lib/simple_drilldown/routing.rb +2 -1
- data/lib/simple_drilldown/version.rb +1 -1
- metadata +8 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c25058c9633cfe67926c2cbf3dca1105481fde8297077ba68414063ab239e05e
|
4
|
+
data.tar.gz: 5ff52f26970deef096886d62451ca80f545d3fbf88601ed20cc2ed3334c01397
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 478e1fa749626a6599055b82022553c5778f8ef4085fa513f28261c9340cb3a112235ff6968115f95de00d4c4af4ec6d9e9b721596edd03693249c5e299e0938
|
7
|
+
data.tar.gz: 2c4b94ae99c33ced23f149fbb0d0f5356867773a13e979619fe0c030ae83fbfac18444579b2b6b78427c48bca1e66a650455640a9d1fe9d95eb2c9629116a2ad
|
data/README.md
CHANGED
@@ -4,9 +4,9 @@
|
|
4
4
|
<img align="right" src="https://travis-ci.org/DatekWireless/simple_drilldown.svg?branch=master" alt="Build Status">
|
5
5
|
</a>
|
6
6
|
|
7
|
-
simple_drilldown offers a simple way to define axis to filter and group records
|
7
|
+
`simple_drilldown` offers a simple way to define axis to filter and group records
|
8
8
|
for analysis. The result is a record count for the selected filter and
|
9
|
-
distribution and the option to list the actual records.
|
9
|
+
distribution and the option to list and export the actual records.
|
10
10
|
|
11
11
|
## Usage
|
12
12
|
|
@@ -16,6 +16,10 @@ For a given schema:
|
|
16
16
|
|
17
17
|
```ruby
|
18
18
|
ActiveRecord::Schema.define(version: 20141204155251) do
|
19
|
+
create_table "users" do |t|
|
20
|
+
t.string "name", limit: 16, null: false
|
21
|
+
end
|
22
|
+
|
19
23
|
create_table "posts" do |t|
|
20
24
|
t.string "title", null: false
|
21
25
|
t.text "body", null: false
|
@@ -25,12 +29,9 @@ ActiveRecord::Schema.define(version: 20141204155251) do
|
|
25
29
|
t.datetime "updated_at", null: false
|
26
30
|
end
|
27
31
|
|
28
|
-
create_table "users" do |t|
|
29
|
-
t.string "name", limit: 16, null: false
|
30
|
-
end
|
31
|
-
|
32
32
|
create_table "comments" do |t|
|
33
33
|
t.integer "post_id", null: false
|
34
|
+
t.integer "user_id", null: false
|
34
35
|
t.string "title", null: false
|
35
36
|
t.text "body", null: false
|
36
37
|
t.integer "rating", null: false
|
@@ -65,6 +66,8 @@ end
|
|
65
66
|
Create a new controller to focus on posts. Each drilldown controller focuses on
|
66
67
|
one main entity.
|
67
68
|
|
69
|
+
bin/rails g drilldown_controller User
|
70
|
+
|
68
71
|
```ruby
|
69
72
|
class PostsDrilldownController < DrilldownController
|
70
73
|
|
@@ -20,7 +20,7 @@
|
|
20
20
|
case @search.display_type
|
21
21
|
when SimpleDrilldown::Search::DisplayType::PIE
|
22
22
|
%>
|
23
|
-
<%= pie_chart data, height: '24rem' %>
|
23
|
+
<%= pie_chart data, height: '24rem', events: ['click'] %>
|
24
24
|
<% when SimpleDrilldown::Search::DisplayType::BAR %>
|
25
25
|
<%= column_chart data, height: '24rem' %>
|
26
26
|
<% when SimpleDrilldown::Search::DisplayType::LINE %>
|
@@ -37,10 +37,10 @@
|
|
37
37
|
<% (0..2).each do |i|
|
38
38
|
options = [['', '']]
|
39
39
|
options << [@dimensions[i][:pretty_name], @dimensions[i][:url_param_name]] if @dimensions[i]
|
40
|
-
options += @remaining_dimensions.keys.map { |name| [
|
40
|
+
options += @remaining_dimensions.keys.map { |name| [controller.c_dimension_defs[name][:pretty_name], name] } %>
|
41
41
|
<%= t(i == 0 ? :group_by : :then_by) %>:
|
42
42
|
<%= form.select 'dimensions', options, { :selected => @search.dimensions && @search.dimensions[i] },
|
43
|
-
{ :
|
43
|
+
{ onChange: 'form.submit()', name: 'search[dimensions][]', id: "search_dimensions_#{i}" } %>
|
44
44
|
<% end %>
|
45
45
|
|
46
46
|
<br/>
|
@@ -60,3 +60,23 @@
|
|
60
60
|
<%= form.check_box :list, { :onChange => 'form.submit()' } %>
|
61
61
|
<%= form.label :list, t(:list) %>
|
62
62
|
</div>
|
63
|
+
|
64
|
+
<% if @dimensions.size == 1 &&
|
65
|
+
[SimpleDrilldown::Search::DisplayType::BAR, SimpleDrilldown::Search::DisplayType::PIE].include?(@search.display_type) %>
|
66
|
+
<%= javascript_tag do %>
|
67
|
+
$('#chart-1').on('click', function(e){
|
68
|
+
chart = Chartkick.charts['chart-1'].getChartObject();
|
69
|
+
firstPoint = chart.getElementsAtEvent(e)[0]
|
70
|
+
if (firstPoint) {
|
71
|
+
label = chart.data.labels[firstPoint._index];
|
72
|
+
value = chart.data.datasets[firstPoint._datasetIndex].data[firstPoint._index];
|
73
|
+
console.log("Label: " + label + ", Value: " + value);
|
74
|
+
new_location = window.location.toString();
|
75
|
+
new_location = new_location.replace("&search[dimensions][]=<%= @dimensions[0][:url_param_name] %>", '');
|
76
|
+
new_location = new_location + '&search[filter][<%= @dimensions[0][:url_param_name] %>][]=' + label;
|
77
|
+
console.log(new_location);
|
78
|
+
window.location = new_location;
|
79
|
+
}
|
80
|
+
});
|
81
|
+
<% end %>
|
82
|
+
<% end %>
|
@@ -1,4 +1,3 @@
|
|
1
|
-
<% dimension = @dimension_defs[dimension_name] %>
|
2
1
|
<% if dimension_name == 'calendar_date' %>
|
3
2
|
<% dates = [*@search.filter[dimension_name]] %>
|
4
3
|
<tr>
|
@@ -23,7 +22,7 @@
|
|
23
22
|
</tr>
|
24
23
|
<% else %>
|
25
24
|
<div class="form-group">
|
26
|
-
<%= form.label "filter[#{dimension_name}]", t(dimension_name) %>
|
25
|
+
<%= form.label "filter[#{dimension_name}]", t(dimension_name, default: :"activerecord.models.#{dimension_name}") %>
|
27
26
|
<%= select :search, :filter,
|
28
27
|
choices,
|
29
28
|
{ :selected => @search.filter[dimension_name] },
|
@@ -6,7 +6,7 @@
|
|
6
6
|
</style>
|
7
7
|
|
8
8
|
<% @transaction_fields.each do | field | %>
|
9
|
-
<%=form.label "fields[#{field}]", t(field), :class => "field_label" %>
|
9
|
+
<%=form.label "fields[#{field}]", t(field, default: :"attributes.#{field}"), :class => "field_label" %>
|
10
10
|
<%=form.check_box :fields, :id => "search_fields[#{field}]", :name => "search[fields][#{field}]", :checked => @search.fields.include?(field) %>
|
11
11
|
<br/>
|
12
12
|
<% end %>
|
@@ -3,18 +3,8 @@
|
|
3
3
|
<%= form.text_field :title, class: 'form-control' %>
|
4
4
|
</div>
|
5
5
|
|
6
|
-
<%
|
7
|
-
<%
|
8
|
-
|
9
|
-
|
10
|
-
<% ActiveRecord::Base.connection_pool.with_connection do %>
|
11
|
-
<% choices[dimension_name] = [[t(:all), nil]] + (dimension[:legal_values] && dimension[:legal_values].call(@search).map { |o| o.is_a?(Array) ? [o[0].to_s, o[1].to_s] : o.to_s } || []) %>
|
12
|
-
<% end %>
|
13
|
-
<% end %>
|
14
|
-
<% next [t, dimension_name] %>
|
15
|
-
<% end %>
|
16
|
-
<% threads.each do |t, dimension_name| %>
|
17
|
-
<% t.join %>
|
18
|
-
<%= render partial: 'drilldown/field', locals: {choices: choices[dimension_name] || [],
|
19
|
-
form: form, dimension_name: dimension_name} %>
|
6
|
+
<% controller.c_dimension_defs.each do |dimension_name, dimension| %>
|
7
|
+
<% choices = [[t(:all), nil]] + (dimension[:legal_values] && dimension[:legal_values].call(@search).map { |o| o.is_a?(Array) ? [o[0].to_s, o[1].to_s] : o.to_s } || []) %>
|
8
|
+
<%= render partial: 'drilldown/field', locals: { choices: choices || [],
|
9
|
+
form: form, dimension_name: dimension_name } %>
|
20
10
|
<% end %>
|
@@ -1,10 +1,10 @@
|
|
1
1
|
<% unless result[:transactions].empty? %>
|
2
2
|
<tr>
|
3
|
-
<td colspan="
|
3
|
+
<td colspan="<%= controller.c_summary_fields.size + 1 %>">
|
4
4
|
<table class="table table-condensed table-bordered" style="padding-bottom: 10px;">
|
5
|
-
<%=render :partial => '/drilldown/row_header' %>
|
5
|
+
<%= render :partial => '/drilldown/row_header' %>
|
6
6
|
<% result[:transactions].each do |t| %>
|
7
|
-
<%=render :partial => '/drilldown/row', :locals => {:transaction => t, :previous_transaction => nil, :errors => [], :error_row => false, :meter1_errors => false} %>
|
7
|
+
<%= render :partial => '/drilldown/row', :locals => { :transaction => t, :previous_transaction => nil, :errors => [], :error_row => false, :meter1_errors => false } %>
|
8
8
|
<% end %>
|
9
9
|
</table>
|
10
10
|
</td>
|
@@ -1,13 +1,13 @@
|
|
1
1
|
<tr valign="top">
|
2
2
|
<% @search.fields.each do |field| %>
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
3
|
+
<td>
|
4
|
+
<% if field == 'time' %>
|
5
|
+
<%= (transaction.respond_to?(:completed_at) ? transaction.completed_at : transaction.created_at).localtime.strftime('%Y-%m-%d %H:%M') %>
|
6
|
+
<% else %>
|
7
|
+
<% field_def = controller.c_fields[field.to_sym] %>
|
8
|
+
<%= field_def[:attr_method] ? field_def[:attr_method].call(transaction) : transaction.send(field) %>
|
9
|
+
<% end %>
|
10
|
+
</td>
|
11
11
|
<% end %>
|
12
|
-
<td><%=
|
12
|
+
<td><%= link_to t(:show), transaction %></td>
|
13
13
|
</tr>
|
@@ -3,8 +3,8 @@
|
|
3
3
|
<% @dimensions.each do |d| %>
|
4
4
|
<th><%=h d[:pretty_name]%></th>
|
5
5
|
<% end %>
|
6
|
-
<th><%= t
|
7
|
-
<%=
|
6
|
+
<th><%= t controller.c_target_class.table_name.capitalize %></th>
|
7
|
+
<%= controller.c_summary_fields.map{|l| "<th>#{t(l)}</th>"}.join("\n").html_safe %>
|
8
8
|
</tr>
|
9
9
|
|
10
10
|
<%=summary_row(@result) %>
|
@@ -45,7 +45,7 @@ xml.Workbook(
|
|
45
45
|
@transactions.each do |transaction|
|
46
46
|
xml.Row do
|
47
47
|
@transaction_fields.each do |field|
|
48
|
-
field_map =
|
48
|
+
field_map = controller.c_fields[field.to_sym]
|
49
49
|
if field == 'time'
|
50
50
|
xml.Cell 'ss:StyleID' => 'DateOnlyFormat' do
|
51
51
|
xml.Data transaction.completed_at.gmtime.xmlschema, 'ss:Type' => 'DateTime'
|
@@ -5,6 +5,7 @@ class DrilldownControllerGenerator < Rails::Generators::NamedBase
|
|
5
5
|
|
6
6
|
def copy_drilldown_controller_file
|
7
7
|
template 'drilldown_controller.rb.erb', "app/controllers/#{file_name}_drilldown_controller.rb"
|
8
|
-
|
8
|
+
template 'drilldown_controller_test.rb.erb', "test/controllers/#{file_name}_drilldown_controller_test.rb"
|
9
|
+
route "draw_drilldown :#{singular_name}_drilldown"
|
9
10
|
end
|
10
11
|
end
|
@@ -1,14 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
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?
|
3
|
+
require 'simple_drilldown/controller'
|
6
4
|
|
5
|
+
class <%= class_name %>DrilldownController < SimpleDrilldown::Controller
|
7
6
|
# The main focus of the drilldown
|
8
|
-
target_class <%= class_name %>
|
7
|
+
# target_class <%= class_name %>
|
8
|
+
|
9
|
+
# `where` clause for the base line
|
10
|
+
# base_condition '1=1'
|
9
11
|
|
10
12
|
# How should we count the reords?
|
11
|
-
select 'count(*) as count'
|
13
|
+
# select 'count(*) as count'
|
14
|
+
|
15
|
+
# When selecting records, what relations should be included for optimization?
|
16
|
+
# Other relations can be included for specific dimensions and fields.
|
17
|
+
# base_includes :user, :comments # TODO(uwe): Read relations from schema?
|
18
|
+
|
19
|
+
# What fields should be displayed as default when listing actual <%= class_name %> records.
|
20
|
+
default_fields %w[created_at updated_at] # TODO(uwe): Read fields from schema?
|
12
21
|
|
13
22
|
# When listing records, what relations should be included for optimization?
|
14
23
|
# list_includes :user, :comments # TODO(uwe): Read relations from schema?
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class <%= class_name %>DrilldownControllerTest < ActionDispatch::IntegrationTest
|
6
|
+
test 'should get index' do
|
7
|
+
get <%= singular_name %>_drilldown_url
|
8
|
+
assert_response :success
|
9
|
+
end
|
10
|
+
|
11
|
+
test 'should get index with list' do
|
12
|
+
get <%= singular_name %>_drilldown_url search: { list: 1 }
|
13
|
+
assert_response :success
|
14
|
+
end
|
15
|
+
end
|
@@ -1,54 +1,84 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'simple_drilldown/
|
3
|
+
require 'simple_drilldown/helper'
|
4
4
|
require 'simple_drilldown/search'
|
5
5
|
|
6
6
|
module SimpleDrilldown
|
7
|
-
class
|
8
|
-
helper
|
7
|
+
class Controller < ::ApplicationController
|
8
|
+
helper Helper
|
9
9
|
|
10
10
|
LIST_LIMIT = 10_000
|
11
11
|
|
12
|
+
class_attribute :c_base_condition, default: '1=1'
|
13
|
+
class_attribute :c_base_group, default: []
|
14
|
+
class_attribute :c_base_includes, default: []
|
15
|
+
class_attribute :c_default_fields, default: []
|
16
|
+
class_attribute :c_default_select_value, default: SimpleDrilldown::Search::SelectValue::COUNT
|
17
|
+
class_attribute :c_dimension_defs
|
18
|
+
class_attribute :c_fields
|
19
|
+
class_attribute :c_list_includes, default: []
|
20
|
+
class_attribute :c_list_order
|
21
|
+
class_attribute :c_select, default: 'count(*) as count'
|
22
|
+
class_attribute :c_summary_fields, default: []
|
23
|
+
class_attribute :c_target_class
|
24
|
+
|
12
25
|
class << self
|
26
|
+
def inherited(base)
|
27
|
+
super
|
28
|
+
base.c_dimension_defs = Concurrent::Hash.new
|
29
|
+
base.c_fields = {}
|
30
|
+
begin
|
31
|
+
base.c_target_class = base.name.chomp('DrilldownController').constantize
|
32
|
+
rescue NameError
|
33
|
+
begin
|
34
|
+
base.c_target_class = base.name.chomp('Controller').constantize
|
35
|
+
rescue NameError
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
13
40
|
def base_condition(base_condition)
|
14
|
-
|
41
|
+
self.c_base_condition = base_condition
|
15
42
|
end
|
16
43
|
|
17
44
|
def base_includes(base_includes)
|
18
|
-
|
45
|
+
self.c_base_includes = base_includes
|
19
46
|
end
|
20
47
|
|
21
48
|
def base_group(base_group)
|
22
|
-
|
49
|
+
self.c_base_group = base_group
|
50
|
+
end
|
51
|
+
|
52
|
+
def default_fields(default_fields)
|
53
|
+
self.c_default_fields = default_fields.flatten
|
23
54
|
end
|
24
55
|
|
25
|
-
def
|
26
|
-
|
56
|
+
def default_select_value(default_select_value)
|
57
|
+
self.c_default_select_value = default_select_value
|
27
58
|
end
|
28
59
|
|
29
60
|
def target_class(target_class)
|
30
|
-
|
61
|
+
self.c_target_class = target_class
|
31
62
|
end
|
32
63
|
|
33
64
|
def select(select)
|
34
|
-
|
65
|
+
self.c_select = select
|
35
66
|
end
|
36
67
|
|
37
68
|
def list_includes(list_includes)
|
38
|
-
|
69
|
+
self.c_list_includes = list_includes.flatten
|
39
70
|
end
|
40
71
|
|
41
72
|
def list_order(list_order)
|
42
|
-
|
73
|
+
self.c_list_order = list_order
|
43
74
|
end
|
44
75
|
|
45
76
|
def field(name, **options)
|
46
|
-
|
47
|
-
@@fields[name] = options
|
77
|
+
c_fields[name] = options
|
48
78
|
end
|
49
79
|
|
50
80
|
def summary_fields(*summary_fields)
|
51
|
-
|
81
|
+
self.c_summary_fields = summary_fields.flatten
|
52
82
|
end
|
53
83
|
|
54
84
|
def dimension(name, select_expression = name, options = {})
|
@@ -74,20 +104,25 @@ module SimpleDrilldown
|
|
74
104
|
raise "Unknown options: #{query_opts.keys.inspect}" unless (query_opts.keys - %i[select includes where]).empty?
|
75
105
|
end
|
76
106
|
|
77
|
-
|
78
|
-
|
79
|
-
@@dimension_defs[name.to_s] = {
|
80
|
-
includes: queries.inject([]) do |a, e|
|
107
|
+
c_dimension_defs[name.to_s] = {
|
108
|
+
includes: queries.inject(nil) do |a, e|
|
81
109
|
i = e[:includes]
|
82
110
|
next a unless i
|
83
|
-
next a if a
|
111
|
+
next a if a&.include?(i)
|
84
112
|
|
85
|
-
a
|
113
|
+
case a
|
114
|
+
when nil
|
115
|
+
i
|
116
|
+
when Symbol
|
117
|
+
[a, *i]
|
118
|
+
else
|
119
|
+
a.concat(*i)
|
120
|
+
end
|
86
121
|
end,
|
87
122
|
interval: interval,
|
88
123
|
label_method: label_method,
|
89
124
|
legal_values: legal_values,
|
90
|
-
pretty_name: I18n.t(name),
|
125
|
+
pretty_name: I18n.t(name, default: :"activerecord.models.#{name}"),
|
91
126
|
queries: queries,
|
92
127
|
reverse: reverse,
|
93
128
|
select_expression: queries.size > 1 ? "COALESCE(#{queries.map { |q| q[:select] }.join(',')})" : queries[0][:select],
|
@@ -101,7 +136,7 @@ module SimpleDrilldown
|
|
101
136
|
my_filter = search.filter.dup
|
102
137
|
my_filter.delete(field.to_s) unless preserve_filter
|
103
138
|
filter_conditions, _t, includes = make_conditions(my_filter)
|
104
|
-
dimension_def =
|
139
|
+
dimension_def = c_dimension_defs[field.to_s]
|
105
140
|
result_sets = dimension_def[:queries].map do |query|
|
106
141
|
if query[:includes]
|
107
142
|
if query[:includes].is_a?(Array)
|
@@ -111,11 +146,11 @@ module SimpleDrilldown
|
|
111
146
|
end
|
112
147
|
includes.uniq!
|
113
148
|
end
|
114
|
-
rows =
|
149
|
+
rows = c_target_class.unscoped.where(c_base_condition)
|
115
150
|
.select("#{query[:select]} AS value")
|
116
|
-
.where(filter_conditions)
|
117
|
-
.where(query[:where])
|
118
|
-
.joins(make_join([],
|
151
|
+
.where(filter_conditions || '1=1')
|
152
|
+
.where(query[:where] || '1=1')
|
153
|
+
.joins(make_join([], c_target_class.name.underscore.to_sym, includes))
|
119
154
|
.order('value')
|
120
155
|
.group(:value)
|
121
156
|
.to_a
|
@@ -137,14 +172,14 @@ module SimpleDrilldown
|
|
137
172
|
end
|
138
173
|
|
139
174
|
def make_conditions(search_filter)
|
140
|
-
includes =
|
175
|
+
includes = c_base_includes.dup
|
141
176
|
if search_filter
|
142
177
|
condition_strings = []
|
143
178
|
condition_values = []
|
144
179
|
|
145
180
|
filter_texts = []
|
146
181
|
search_filter.each do |field, values|
|
147
|
-
dimension_def =
|
182
|
+
dimension_def = c_dimension_defs[field]
|
148
183
|
raise "Unknown filter field: #{field.inspect}" if dimension_def.nil?
|
149
184
|
|
150
185
|
values = [*values]
|
@@ -202,9 +237,9 @@ module SimpleDrilldown
|
|
202
237
|
when Hash
|
203
238
|
sql = +''
|
204
239
|
include.each do |parent, child|
|
205
|
-
sql << make_join(joins, model, parent)
|
240
|
+
sql << ' ' + make_join(joins, model, parent)
|
206
241
|
ass = model.to_s.camelize.constantize.reflect_on_association parent
|
207
|
-
sql << make_join(joins, parent, child, ass.class_name.constantize)
|
242
|
+
sql << ' ' + make_join(joins, parent, child, ass.class_name.constantize)
|
208
243
|
end
|
209
244
|
sql
|
210
245
|
when Symbol
|
@@ -254,24 +289,7 @@ module SimpleDrilldown
|
|
254
289
|
|
255
290
|
def initialize
|
256
291
|
super()
|
257
|
-
@
|
258
|
-
@default_fields = @@default_fields
|
259
|
-
@default_select_value = SimpleDrilldown::Search::SelectValue::COUNT
|
260
|
-
@target_class = @@target_class
|
261
|
-
@select = @@select
|
262
|
-
@@base_condition = '1 = 1' unless defined?(@@base_condition)
|
263
|
-
@base_condition = @@base_condition
|
264
|
-
@@base_includes = [] unless defined?(@@base_includes)
|
265
|
-
@base_includes = @@base_includes
|
266
|
-
@base_group = defined?(@@base_group) ? @@base_group : []
|
267
|
-
@@list_includes = [] unless defined?(@@list_includes)
|
268
|
-
@list_includes = @@list_includes
|
269
|
-
@list_order = @@list_order
|
270
|
-
@dimension_defs = @@dimension_defs
|
271
|
-
@@summary_fields = [] unless defined?(@@summary_fields)
|
272
|
-
@summary_fields = @@summary_fields
|
273
|
-
|
274
|
-
@history_fields = @fields.select { |_k, v| v[:list_change_times] }.map { |k, _v| k.to_s }
|
292
|
+
@history_fields = c_fields.select { |_k, v| v[:list_change_times] }.map { |k, _v| k.to_s }
|
275
293
|
end
|
276
294
|
|
277
295
|
# ?dimension[0]=supplier&dimension[1]=transaction_type&
|
@@ -279,18 +297,17 @@ module SimpleDrilldown
|
|
279
297
|
def index(do_render = true)
|
280
298
|
@search = new_search_object
|
281
299
|
|
282
|
-
@transaction_fields = (@search.fields + (
|
283
|
-
@transaction_fields_map = @fields
|
300
|
+
@transaction_fields = (@search.fields + (c_fields.keys.map(&:to_s) - @search.fields))
|
284
301
|
|
285
|
-
select =
|
286
|
-
includes =
|
302
|
+
select = c_select.dup
|
303
|
+
includes = c_base_includes.dup
|
287
304
|
|
288
305
|
@dimensions = []
|
289
|
-
select << ", 'All'::text as value0"
|
306
|
+
select << ", 'All'#{'::text' if c_target_class.connection.adapter_name == 'PostgreSQL'} as value0"
|
290
307
|
@dimensions += @search.dimensions.map do |dn|
|
291
|
-
raise "Unknown distribution field: #{dn.inspect}" if
|
308
|
+
raise "Unknown distribution field: #{dn.inspect}" if c_dimension_defs[dn].nil?
|
292
309
|
|
293
|
-
|
310
|
+
c_dimension_defs[dn]
|
294
311
|
end
|
295
312
|
@dimensions.each_with_index do |d, i|
|
296
313
|
select << ", #{d[:select_expression]} as value#{i + 1}"
|
@@ -315,18 +332,18 @@ module SimpleDrilldown
|
|
315
332
|
order = (1..@dimensions.size).map { |i| "value#{i}" }.join(',')
|
316
333
|
order = nil if order.empty?
|
317
334
|
end
|
318
|
-
group = (
|
335
|
+
group = (c_base_group + (1..@dimensions.size).map { |i| "value#{i}" }).join(',')
|
319
336
|
group = nil if group.empty?
|
320
337
|
|
321
|
-
joins = self.class.make_join([],
|
322
|
-
rows =
|
323
|
-
|
324
|
-
|
325
|
-
|
338
|
+
joins = self.class.make_join([], c_target_class.name.underscore.to_sym, includes)
|
339
|
+
rows = c_target_class.unscoped.where(c_base_condition).select(select).where(conditions)
|
340
|
+
.joins(joins)
|
341
|
+
.group(group)
|
342
|
+
.order(order).to_a
|
326
343
|
|
327
344
|
if rows.empty?
|
328
345
|
@result = { value: 'All', count: 0, row_count: 0, nodes: 0, rows: [] }
|
329
|
-
|
346
|
+
c_summary_fields.each { |f| @result[f] = 0 }
|
330
347
|
else
|
331
348
|
if do_render && @search.list && rows.inject(0) { |sum, r| sum + r[:count].to_i } > LIST_LIMIT
|
332
349
|
@search.list = false
|
@@ -335,9 +352,9 @@ module SimpleDrilldown
|
|
335
352
|
@result = result_from_rows(rows, 0, 0, ['All'])
|
336
353
|
end
|
337
354
|
|
338
|
-
remove_duplicates(@result) unless
|
355
|
+
remove_duplicates(@result) unless c_base_group.empty?
|
339
356
|
|
340
|
-
@remaining_dimensions =
|
357
|
+
@remaining_dimensions = c_dimension_defs.dup
|
341
358
|
@remaining_dimensions.each_key do |dim_name|
|
342
359
|
if (@search.filter[dim_name] && @search.filter[dim_name].size == 1) ||
|
343
360
|
(@dimensions.any? { |d| d[:url_param_name] == dim_name })
|
@@ -352,9 +369,9 @@ module SimpleDrilldown
|
|
352
369
|
def choices
|
353
370
|
@search = new_search_object
|
354
371
|
dimension_name = params[:dimension_name]
|
355
|
-
dimension =
|
372
|
+
dimension = c_dimension_defs[dimension_name]
|
356
373
|
selected = @search.filter[dimension_name] || []
|
357
|
-
raise "Unknown dimension #{dimension_name.inspect}: #{
|
374
|
+
raise "Unknown dimension #{dimension_name.inspect}: #{c_dimension_defs.keys.inspect}" unless dimension
|
358
375
|
|
359
376
|
choices = [[t(:all), nil]] +
|
360
377
|
(dimension[:legal_values]&.call(@search)&.map { |o| o.is_a?(Array) ? o[0..1].map(&:to_s) : o.to_s } || [])
|
@@ -398,7 +415,7 @@ module SimpleDrilldown
|
|
398
415
|
private
|
399
416
|
|
400
417
|
def new_search_object
|
401
|
-
SimpleDrilldown::Search.new(params[:search]&.to_unsafe_h,
|
418
|
+
SimpleDrilldown::Search.new(params[:search]&.to_unsafe_h, c_default_fields, c_default_select_value)
|
402
419
|
end
|
403
420
|
|
404
421
|
def remove_duplicates(result)
|
@@ -411,7 +428,7 @@ module SimpleDrilldown
|
|
411
428
|
if prev_row
|
412
429
|
if prev_row[:value] == r[:value]
|
413
430
|
prev_row[:count] += r[:count]
|
414
|
-
|
431
|
+
c_summary_fields.each do |f|
|
415
432
|
prev_row[f] += r[f]
|
416
433
|
end
|
417
434
|
prev_row[:row_count] = [prev_row[:row_count], r[:row_count]].max
|
@@ -449,7 +466,7 @@ module SimpleDrilldown
|
|
449
466
|
row_count: 0,
|
450
467
|
nodes: 0
|
451
468
|
}
|
452
|
-
|
469
|
+
c_summary_fields.each { |f| sub_result[f] = 0 }
|
453
470
|
sub_result[:rows] = add_zero_results([], dimension + 1) if dimension < @dimensions.size - 1
|
454
471
|
result_rows << sub_result
|
455
472
|
end
|
@@ -472,7 +489,7 @@ module SimpleDrilldown
|
|
472
489
|
row_count: 1,
|
473
490
|
nodes: @search.list ? 2 : 1
|
474
491
|
}
|
475
|
-
|
492
|
+
c_summary_fields.each { |f| result[f] = row[f].to_i }
|
476
493
|
return result
|
477
494
|
end
|
478
495
|
|
@@ -495,37 +512,75 @@ module SimpleDrilldown
|
|
495
512
|
nodes: result_rows.inject(0) { |t, r| t + r[:nodes] } + 1,
|
496
513
|
rows: result_rows
|
497
514
|
}
|
498
|
-
|
515
|
+
c_summary_fields.each { |f| result[f] = result_rows.inject(0) { |t, r| t + r[f] } }
|
499
516
|
result
|
500
517
|
end
|
501
518
|
|
502
519
|
def populate_list(conditions, includes, result, values)
|
503
520
|
if result[:rows]
|
504
|
-
result[:rows].each
|
505
|
-
|
521
|
+
return result[:rows].each { |r| populate_list(conditions, includes, r, values + [r[:value]]) }
|
522
|
+
end
|
523
|
+
list_includes = merge_includes(includes, c_list_includes)
|
524
|
+
@search.fields.each do |field|
|
525
|
+
field_def = c_fields[field.to_sym]
|
526
|
+
raise "Field definition missing for: #{field.inspect}" unless field_def
|
527
|
+
|
528
|
+
field_includes = field_def[:include]
|
529
|
+
if field_includes
|
530
|
+
list_includes = merge_includes(list_includes , field_includes)
|
506
531
|
end
|
507
|
-
|
508
|
-
|
509
|
-
@
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
field_includes = field_def[:include]
|
514
|
-
if field_includes
|
515
|
-
list_includes += field_includes.is_a?(Array) ? field_includes : [field_includes]
|
532
|
+
end
|
533
|
+
if @search.list_change_times
|
534
|
+
@history_fields.each do |f|
|
535
|
+
if @search.fields.include? f
|
536
|
+
list_includes = merge_includes(list_includes, assignment: { order: :"#{f}_changes" } )
|
516
537
|
end
|
517
538
|
end
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
539
|
+
end
|
540
|
+
joins = self.class.make_join([], c_target_class.name.underscore.to_sym, list_includes)
|
541
|
+
list_conditions = list_conditions(conditions, values)
|
542
|
+
base_query = c_target_class.unscoped.where(c_base_condition).joins(joins).order(c_list_order)
|
543
|
+
base_query = base_query.where(list_conditions) if list_conditions
|
544
|
+
result[:transactions] = base_query.to_a
|
545
|
+
end
|
546
|
+
|
547
|
+
def merge_includes(*args)
|
548
|
+
hash = hash_includes(*args)
|
549
|
+
result = hash.dup.map { |k, v|
|
550
|
+
if v.blank?
|
551
|
+
hash.delete(k)
|
552
|
+
k
|
553
|
+
end
|
554
|
+
}.compact
|
555
|
+
result << hash unless hash.blank?
|
556
|
+
case result.size
|
557
|
+
when 0
|
558
|
+
nil
|
559
|
+
when 1
|
560
|
+
result[0]
|
561
|
+
else
|
562
|
+
result
|
563
|
+
end
|
564
|
+
end
|
565
|
+
|
566
|
+
def hash_includes(*args)
|
567
|
+
args.inject({}) do |h, inc|
|
568
|
+
case inc
|
569
|
+
when Array
|
570
|
+
inc.each { |v|
|
571
|
+
h = hash_includes(h, v)
|
572
|
+
}
|
573
|
+
when Hash
|
574
|
+
inc.each { |k, v|
|
575
|
+
h[k] = merge_includes(h[k], v)
|
576
|
+
}
|
577
|
+
when NilClass, FalseClass
|
578
|
+
when String, Symbol
|
579
|
+
h[inc] ||= []
|
580
|
+
else
|
581
|
+
raise "Unknown include type: #{inc.inspect}"
|
523
582
|
end
|
524
|
-
|
525
|
-
list_conditions = list_conditions(conditions, values)
|
526
|
-
base_query = @target_class.unscoped.where(@base_condition).joins(joins).order(@list_order)
|
527
|
-
base_query = base_query.where(list_conditions) if list_conditions
|
528
|
-
result[:transactions] = base_query.to_a
|
583
|
+
h
|
529
584
|
end
|
530
585
|
end
|
531
586
|
|
@@ -1,11 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'chartkick'
|
4
|
+
require 'simple_drilldown/routing'
|
5
|
+
|
3
6
|
module SimpleDrilldown
|
4
7
|
class Engine < ::Rails::Engine
|
5
8
|
isolate_namespace SimpleDrilldown
|
9
|
+
config.autoload_paths << File.dirname(__dir__)
|
6
10
|
|
7
11
|
initializer 'simple_drilldown.assets.precompile' do |app|
|
8
12
|
app.config.assets.precompile += %w[chartkick.js]
|
9
13
|
end
|
14
|
+
|
15
|
+
ActionDispatch::Routing::Mapper.include SimpleDrilldown::Routing
|
10
16
|
end
|
11
17
|
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module SimpleDrilldown
|
4
4
|
# View helper for SimpleDrilldown
|
5
|
-
module
|
5
|
+
module Helper
|
6
6
|
def value_label(dimension_index, value)
|
7
7
|
dimension = @dimensions[dimension_index]
|
8
8
|
return nil if dimension.nil?
|
@@ -11,7 +11,7 @@ module SimpleDrilldown
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def caption
|
14
|
-
result = @search.title || "#{
|
14
|
+
result = @search.title || "#{controller.c_target_class} #{t(@search.select_value.downcase)}" +
|
15
15
|
(@dimensions && @dimensions.any? ? ' by ' + @dimensions.map { |d| d[:pretty_name] }.join(' and ') : '')
|
16
16
|
result.gsub('$date', [*@search.filter[:calendar_date]].uniq.join(' - '))
|
17
17
|
end
|
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module SimpleDrilldown
|
4
|
+
# Routing helper methods
|
4
5
|
module Routing
|
5
6
|
def draw_drilldown(path, controller = path)
|
6
7
|
get "#{path}(.:format)" => "#{controller}#index", as: path
|
7
8
|
scope path do
|
8
|
-
%i[excel_export html_export index].each do |action|
|
9
|
+
%i[choices excel_export html_export index].each do |action|
|
9
10
|
get "#{action}(/:id)(.:format)", controller: controller, action: action
|
10
11
|
end
|
11
12
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simple_drilldown
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4
|
4
|
+
version: 0.6.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Uwe Kubosch
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-09-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: chartkick
|
@@ -123,10 +123,11 @@ files:
|
|
123
123
|
- lib/generators/drilldown_controller/USAGE
|
124
124
|
- lib/generators/drilldown_controller/drilldown_controller_generator.rb
|
125
125
|
- lib/generators/drilldown_controller/templates/drilldown_controller.rb.erb
|
126
|
+
- lib/generators/drilldown_controller/templates/drilldown_controller_test.rb.erb
|
126
127
|
- lib/simple_drilldown.rb
|
127
|
-
- lib/simple_drilldown/
|
128
|
-
- lib/simple_drilldown/drilldown_helper.rb
|
128
|
+
- lib/simple_drilldown/controller.rb
|
129
129
|
- lib/simple_drilldown/engine.rb
|
130
|
+
- lib/simple_drilldown/helper.rb
|
130
131
|
- lib/simple_drilldown/routing.rb
|
131
132
|
- lib/simple_drilldown/search.rb
|
132
133
|
- lib/simple_drilldown/version.rb
|
@@ -136,7 +137,7 @@ licenses:
|
|
136
137
|
- MIT
|
137
138
|
metadata:
|
138
139
|
allowed_push_host: https://rubygems.org/
|
139
|
-
post_install_message:
|
140
|
+
post_install_message:
|
140
141
|
rdoc_options: []
|
141
142
|
require_paths:
|
142
143
|
- lib
|
@@ -152,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
152
153
|
version: '0'
|
153
154
|
requirements: []
|
154
155
|
rubygems_version: 3.1.2
|
155
|
-
signing_key:
|
156
|
+
signing_key:
|
156
157
|
specification_version: 4
|
157
158
|
summary: Simple data warehouse and drilldown.
|
158
159
|
test_files: []
|