simple_drilldown 0.3.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1268ff5130d0858fb19cefca5ea728950e44c44ecb41bbee08d29cd9a6118d8
4
- data.tar.gz: 3bcea3e030470e8b35be748a9b21f298f13a22b2b3a4533c5f88a9844907ffbc
3
+ metadata.gz: 3a69810eb60aa499744a6989bad20efed263b05fc51dfcb094f151494deacbad
4
+ data.tar.gz: e3402d01f9a8a19683c6c4095fa47725eede9dda5434ff2dd9a43cb5d5e75643
5
5
  SHA512:
6
- metadata.gz: '07417628dc35edba1c7e7dd6a14112573f8950cc83a6aa9d4517eb285eb30e1ec6a09e0e7815f07208582e37c5bcf4c5b15adbad611e1aeedc74c89a4149597d'
7
- data.tar.gz: 7a16880f4c0b22393ce2e6ff7f04bd78fde735fb1a85eca15d392f2c8e3de0488f675ab19ad61a7c2733b45619b7f2e32d4dbcb83f51845bda33e51b845f25e4
6
+ metadata.gz: 7701b13e4ee048dd707b9e5a787e2081932ad301cb62eb028e770033664a6d7696267fd2a409a79a540db73a06026305b2bd2eee597fdd8196c7568db161d2f6
7
+ data.tar.gz: 5a9a820ecee0516f76bebb0a3ce64325a80e0e3f71f4855b2529ab07b451cf87db2148544114343f40d5f35e624bf5bac8c614ca600642ca8127b62f6ffc072c
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
 
@@ -0,0 +1,62 @@
1
+ <%
2
+ data =
3
+ case @dimensions.size
4
+ when 0
5
+ { @result[:value] => @result[:count] }
6
+ when 1
7
+ @result[:rows].map { |r| [@dimensions[0][:label_method] ? @dimensions[0][:label_method].call(r[:value]) : r[:value], r[:count]] }
8
+ when 2
9
+ @result[:rows].map do |r|
10
+ {
11
+ name: r[:value],
12
+ data: r[:rows].map { |r2| [r2[:value], r2[:count]] }
13
+ }
14
+ end
15
+ when 3
16
+ end
17
+ %>
18
+
19
+ <%
20
+ case @search.display_type
21
+ when SimpleDrilldown::Search::DisplayType::PIE
22
+ %>
23
+ <%= pie_chart data, height: '24rem' %>
24
+ <% when SimpleDrilldown::Search::DisplayType::BAR %>
25
+ <%= column_chart data, height: '24rem' %>
26
+ <% when SimpleDrilldown::Search::DisplayType::LINE %>
27
+ <%= line_chart data, height: '24rem' %>
28
+ <% else %>
29
+ <div id="drilldown_area">
30
+ <h2><%= caption %></h2>
31
+ <h3><%= subcaption %></h3>
32
+ <br/>
33
+ </div>
34
+ <% end %>
35
+
36
+ <div id="drilldown_search_area" style="margin-left: auto; margin-right: auto; text-align: center">
37
+ <% (0..2).each do |i|
38
+ options = [['', '']]
39
+ options << [@dimensions[i][:pretty_name], @dimensions[i][:url_param_name]] if @dimensions[i]
40
+ options += @remaining_dimensions.keys.map { |name| [controller.c_dimension_defs[name][:pretty_name], name] } %>
41
+ <%= t(i == 0 ? :group_by : :then_by) %>:
42
+ <%= form.select 'dimensions', options, { :selected => @search.dimensions && @search.dimensions[i] },
43
+ { :onChange => 'form.submit()', :name => 'search[dimensions][]', :id => "search_dimensions_#{i}" } %>
44
+ <% end %>
45
+
46
+ <br/>
47
+ <%= t :chart_type %>
48
+ <%= form.radio_button 'display_type', SimpleDrilldown::Search::DisplayType::BAR, { :onChange => 'form.submit()' } %>
49
+ <%= form.label :display_type_bar, t(:bar) %>
50
+ <%= form.radio_button 'display_type', SimpleDrilldown::Search::DisplayType::PIE, { :disabled => @search.dimensions.size >= 2, :onChange => 'form.submit()' } %>
51
+ <%= form.label :display_type_pie, t(:pie) %>
52
+ <%= form.radio_button 'display_type', SimpleDrilldown::Search::DisplayType::LINE, { :onChange => 'form.submit()' } %>
53
+ <%= form.label :display_type_line, t(:line) %>
54
+ <%= form.radio_button 'display_type', SimpleDrilldown::Search::DisplayType::NONE, { :onChange => 'form.submit()' } %>
55
+ <%= form.label :display_type_none, t(:none) %>
56
+
57
+ <%= form.check_box :order_by_value, { :onChange => 'form.submit()' } %>
58
+ <%= form.label :order_by_value, t(:order_by_value) %>
59
+
60
+ <%= form.check_box :list, { :onChange => 'form.submit()' } %>
61
+ <%= form.label :list, t(:list) %>
62
+ </div>
@@ -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>
@@ -3,18 +3,8 @@
3
3
  <%= form.text_field :title, class: 'form-control' %>
4
4
  </div>
5
5
 
6
- <% choices = Concurrent::Hash.new %>
7
- <% threads = @dimension_defs.keys.map do |dimension_name| %>
8
- <% dimension = @dimension_defs[dimension_name] %>
9
- <% t = Thread.start do |thread| %>
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="<%=@summary_fields.size + 1%>">
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
- <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 = @transaction_fields_map[field.to_sym] %>
8
- <%= field_def[:attr_method] ? field_def[:attr_method].call(transaction) : transaction.send(field) %>
9
- <% end %>
10
- </td>
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><%= detour_to t(:show), transaction %></td>
12
+ <td><%= link_to t(:show), transaction %></td>
13
13
  </tr>
@@ -1,18 +1,18 @@
1
1
  <% if new_row -%>
2
- <tr class="<%= cycle("odd", "even", :name => "dim#{dimension}") %>">
3
- <% ((dimension + 1)..(@dimensions.size)).each { |i| cycle("odd", "even", :name => "dim#{i}") if current_cycle("dim#{i}") != current_cycle("dim#{dimension}") } -%>
2
+ <tr class="<%= cycle("odd", "even", :name => "dim#{dimension}") %>">
3
+ <% ((dimension + 1)..(@dimensions.size)).each { |i| cycle("odd", "even", :name => "dim#{i}") if current_cycle("dim#{i}") != current_cycle("dim#{dimension}") } -%>
4
4
  <% end -%>
5
5
  <% if dimension > 0 %>
6
- <td valign="top" rowspan="<%= result[:nodes] %>">
7
- <%= link_to value_label(dimension - 1, result[:value]), @search.drill_down(@dimensions, *[headers[1..-1], result].flatten.map { |h| h[:value] }).url_options %>
8
- </td>
6
+ <td valign="top" rowspan="<%= result[:nodes] %>">
7
+ <%= link_to value_label(dimension - 1, result[:value]), @search.drill_down(@dimensions, *[headers[1..-1], result].flatten.map { |h| h[:value] }).url_options %>
8
+ </td>
9
9
  <% end %>
10
10
  <% if with_results %>
11
- <td align="right">
12
- <%= result[:count] %>
13
- <% if parent_result && @search.percent %>
14
- (<%= 100 * result[:count] / parent_result[:count] %>%)
15
- <% end %>
16
- </td>
17
- </tr>
11
+ <td align="right">
12
+ <%= result[:count] %>
13
+ <% if parent_result && @search.percent %>
14
+ (<%= 100 * result[:count] / parent_result[:count] %>%)
15
+ <% end %>
16
+ </td>
17
+ </tr>
18
18
  <% end %>
@@ -3,8 +3,8 @@
3
3
  <% @dimensions.each do |d| %>
4
4
  <th><%=h d[:pretty_name]%></th>
5
5
  <% end %>
6
- <th><%= t @target_class.table_name.capitalize %></th>
7
- <%= @summary_fields.map{|l| "<th>#{t(l)}</th>"}.join("\n").html_safe %>
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 = @transaction_fields_map[field.to_sym]
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'
@@ -6,3 +6,4 @@ Example:
6
6
 
7
7
  This will create:
8
8
  app/controllers/thing_drilldown_controller.rb
9
+ test/controllers/thing_drilldown_controller_test.rb
@@ -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
- route "resources(:#{singular_name}_drilldown, only: :index){collection{get :excel_export;get :html_export}}"
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
- 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?
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?
@@ -27,14 +36,15 @@ class <%= class_name %>DrilldownController < DrilldownController
27
36
  # field :comments, attr_method: ->(post) { post.comments.count }
28
37
 
29
38
  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)",
39
+ dimension :day_of_month, "date_part('day', <%= plural_name %>.created_at)::int"
40
+ dimension :day_of_week, <<~SQL, label_method: ->(day_no) { Date::DAYNAMES[day_no.to_i % 7] }
41
+ CASE WHEN date_part('dow', <%= plural_name %>.created_at) = 0 THEN 7 ELSE date_part('dow', <%= plural_name %>.created_at)::int END
42
+ SQL
43
+ dimension :hour_of_day, "date_part('hour', <%= plural_name %>.created_at)::int"
44
+ dimension :month, "date_part('month', <%= plural_name %>.created_at)::int",
35
45
  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)"
46
+ dimension :week, "date_part('week', <%= plural_name %>.created_at)::int"
47
+ dimension :year, "date_part('year', <%= plural_name %>.created_at)::varchar"
38
48
 
39
49
  # dimension :comments, 'SELECT count(*) FROM comments c WHERE c.<%= singular_name %>_id = <%= plural_name %>.id'
40
50
  # dimension :user, 'users.name', includes: :user
@@ -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
@@ -4,51 +4,81 @@ require 'simple_drilldown/drilldown_helper'
4
4
  require 'simple_drilldown/search'
5
5
 
6
6
  module SimpleDrilldown
7
- class DrilldownController < ::ApplicationController
7
+ class Controller < ::ApplicationController
8
8
  helper DrilldownHelper
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
- @@base_condition = base_condition
41
+ self.c_base_condition = base_condition
15
42
  end
16
43
 
17
44
  def base_includes(base_includes)
18
- @@base_includes = base_includes
45
+ self.c_base_includes = base_includes
19
46
  end
20
47
 
21
48
  def base_group(base_group)
22
- @@base_group = base_group
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 default_fields(fields)
26
- @@default_fields = fields
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
- @@target_class = target_class
61
+ self.c_target_class = target_class
31
62
  end
32
63
 
33
64
  def select(select)
34
- @@select = select
65
+ self.c_select = select
35
66
  end
36
67
 
37
68
  def list_includes(list_includes)
38
- @@list_includes = list_includes
69
+ self.c_list_includes = list_includes.flatten
39
70
  end
40
71
 
41
72
  def list_order(list_order)
42
- @@list_order = list_order
73
+ self.c_list_order = list_order
43
74
  end
44
75
 
45
76
  def field(name, **options)
46
- @@fields ||= {}
47
- @@fields[name] = options
77
+ c_fields[name] = options
48
78
  end
49
79
 
50
80
  def summary_fields(*summary_fields)
51
- @@summary_fields = summary_fields
81
+ self.c_summary_fields = summary_fields.flatten
52
82
  end
53
83
 
54
84
  def dimension(name, select_expression = name, options = {})
@@ -74,15 +104,20 @@ 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
- @@dimension_defs ||= Concurrent::Hash.new
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.include?(i)
111
+ next a if a&.include?(i)
84
112
 
85
- a + [i]
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,
@@ -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 = @@dimension_defs[field.to_s]
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 = @@target_class.unscoped.where(@@base_condition)
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([], @@target_class.name.underscore.to_sym, includes))
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 = @@base_includes.dup
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 = @@dimension_defs[field]
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
- @fields = @@fields
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 + (@fields.keys.map(&:to_s) - @search.fields))
283
- @transaction_fields_map = @fields
300
+ @transaction_fields = (@search.fields + (c_fields.keys.map(&:to_s) - @search.fields))
284
301
 
285
- select = @select.dup
286
- includes = @base_includes.dup
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 @dimension_defs[dn].nil?
308
+ raise "Unknown distribution field: #{dn.inspect}" if c_dimension_defs[dn].nil?
292
309
 
293
- @dimension_defs[dn]
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}"
@@ -302,11 +319,11 @@ module SimpleDrilldown
302
319
  includes.keep_if(&:present?).uniq!
303
320
  if @search.order_by_value && @dimensions.size <= 1
304
321
  order = case @search.select_value
305
- when DrilldownSearch::SelectValue::VOLUME
322
+ when Search::SelectValue::VOLUME
306
323
  'volume DESC'
307
- when DrilldownSearch::SelectValue::VOLUME_COMPENSATED
324
+ when Search::SelectValue::VOLUME_COMPENSATED
308
325
  'volume_compensated DESC'
309
- when DrilldownSearch::SelectValue::COUNT
326
+ when Search::SelectValue::COUNT
310
327
  'count DESC'
311
328
  else
312
329
  'count DESC'
@@ -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 = (@base_group + (1..@dimensions.size).map { |i| "value#{i}" }).join(',')
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([], @target_class.name.underscore.to_sym, includes)
322
- rows = @target_class.unscoped.where(@base_condition).select(select).where(conditions)
323
- .joins(joins)
324
- .group(group)
325
- .order(order).to_a
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
- @summary_fields.each { |f| @result[f] = 0 }
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 @base_group.empty?
355
+ remove_duplicates(@result) unless c_base_group.empty?
339
356
 
340
- @remaining_dimensions = @dimension_defs.dup
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 = @dimension_defs[dimension_name]
372
+ dimension = c_dimension_defs[dimension_name]
356
373
  selected = @search.filter[dimension_name] || []
357
- raise "Unknown dimension #{dimension_name.inspect}: #{@dimension_defs.keys.inspect}" unless dimension
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, @default_fields, @default_select_value)
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
- @summary_fields.each do |f|
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
- @summary_fields.each { |f| sub_result[f] = 0 }
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
- @summary_fields.each { |f| result[f] = row[f].to_i }
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
- @summary_fields.each { |f| result[f] = result_rows.inject(0) { |t, r| t + r[f] } }
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 do |r|
505
- populate_list(conditions, includes, r, values + [r[:value]])
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
- else
508
- list_includes = includes + @list_includes
509
- @search.fields.each do |field|
510
- field_def = @transaction_fields_map[field.to_sym]
511
- raise "Field definition missing for: #{field.inspect}" unless field_def
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
- list_includes.uniq!
519
- if @search.list_change_times
520
- @history_fields.each do |f|
521
- list_includes << { assignment: { order: :"#{f}_changes" } } if @search.fields.include? f
522
- end
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(@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
- joins = self.class.make_join([], @target_class.name.underscore.to_sym, list_includes)
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,67 +1,65 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module SimpleDrilldown
4
+ # View helper for SimpleDrilldown
5
+ # FIXME(uwe): Rename to Helper
2
6
  module DrilldownHelper
3
7
  def value_label(dimension_index, value)
4
- return nil if @dimensions[dimension_index].nil?
5
- h(@dimensions[dimension_index][:label_method] ?
6
- @dimensions[dimension_index][:label_method].call(value) :
7
- value)
8
+ dimension = @dimensions[dimension_index]
9
+ return nil if dimension.nil?
10
+
11
+ h(dimension[:label_method] ? dimension[:label_method].call(value) : value)
8
12
  end
9
13
 
10
14
  def caption
11
- result = @search.title ? @search.title : "#{@target_class} #{t(@search.select_value.downcase)}" +
12
- ((@dimensions && @dimensions.any?) ? ' by ' + @dimensions.map { |d| d[:pretty_name] }.join(' and ') : '')
15
+ result = @search.title || "#{controller.c_target_class} #{t(@search.select_value.downcase)}" +
16
+ (@dimensions && @dimensions.any? ? ' by ' + @dimensions.map { |d| d[:pretty_name] }.join(' and ') : '')
13
17
  result.gsub('$date', [*@search.filter[:calendar_date]].uniq.join(' - '))
14
18
  end
15
19
 
16
20
  def subcaption
17
- @search.title ? '' : @filter_text && @filter_text.size > 0 ? "for #{@filter_text}" : ''
21
+ @search.title || @filter_text.blank? ? '' : "for #{@filter_text}"
18
22
  end
19
23
 
20
24
  def summary_row(result, parent_result = nil, dimension = 0, headers = [], new_row = true)
21
- html = render(:partial => '/drilldown/summary_row', :locals => { :result => result, :parent_result => parent_result, :new_row => new_row, :dimension => dimension, :headers => headers, :with_results => !result[:rows] })
25
+ html = render(partial: '/drilldown/summary_row', locals: { result: result, parent_result: parent_result, new_row: new_row, dimension: dimension, headers: headers, with_results: !result[:rows] })
22
26
  if result[:rows]
23
- sub_headers = headers + [{ :value => result[:value], :display_row_count => result[:nodes] + result[:row_count] * (@search.list ? 1 : 0) }]
24
- significant_rows = result[:rows].select { |r| r[:row_count] != 0 }
27
+ sub_headers = headers + [{ value: result[:value], display_row_count: result[:nodes] + result[:row_count] * (@search.list ? 1 : 0) }]
28
+ significant_rows = result[:rows].reject { |r| r[:row_count].zero? }
25
29
  significant_rows.each_with_index do |r, i|
26
- html << summary_row(r, result, dimension + 1, sub_headers, i > 0)
30
+ html << summary_row(r, result, dimension + 1, sub_headers, i.positive?)
27
31
  end
28
32
  elsif @search.list
29
- html << render(:partial => '/drilldown/record_list', :locals => { :result => result })
30
- end
31
- if dimension < @dimensions.size
32
- html << render(:partial => '/drilldown/summary_total_row', :locals => { :result => result, :parent_result => parent_result, :headers => headers.dup, :dimension => dimension })
33
+ html << render(partial: '/drilldown/record_list', locals: { result: result })
33
34
  end
35
+ html << render(partial: '/drilldown/summary_total_row', locals: { result: result, parent_result: parent_result, headers: headers.dup, dimension: dimension }) if dimension < @dimensions.size
34
36
 
35
37
  html
36
38
  end
37
39
 
38
40
  def excel_summary_row(result, parent_result = nil, dimension = 0, headers = [])
39
- xml = ''
41
+ xml = +''
40
42
  if result[:rows]
41
- significant_rows = result[:rows].select { |r| r[:row_count] != 0 }
43
+ significant_rows = result[:rows].reject { |r| r[:row_count].zero? }
42
44
  significant_rows.each_with_index do |r, i|
43
- if i == 0
44
- if dimension == 0
45
- sub_headers = headers
46
- else
47
- sub_headers = headers + [{ :value => result[:value], :display_row_count => result[:nodes] + result[:row_count] * (@search.list ? 1 : 0) }]
48
- end
49
- else
50
- sub_headers = [] # [{:value => result[:value], :row_count => result[:row_count]}]
51
- end
45
+ sub_headers = if i.zero?
46
+ if dimension.zero?
47
+ headers
48
+ else
49
+ headers + [{ value: result[:value], display_row_count: result[:nodes] + result[:row_count] * (@search.list ? 1 : 0) }]
50
+ end
51
+ else
52
+ [] # [{:value => result[:value], :row_count => result[:row_count]}]
53
+ end
52
54
  xml << excel_summary_row(r, result, dimension + 1, sub_headers)
53
55
  end
54
56
  else
55
- xml << render(:partial => '/drilldown/excel_summary_row', :locals => { :result => result, :parent_result => parent_result, :headers => headers.dup, :dimension => dimension })
57
+ xml << render(partial: '/drilldown/excel_summary_row', locals: { result: result, parent_result: parent_result, headers: headers.dup, dimension: dimension })
56
58
 
57
- if @search.list
58
- xml << render(:partial => '/drilldown/excel_record_list', :locals => { :result => result })
59
- end
59
+ xml << render(partial: '/drilldown/excel_record_list', locals: { result: result }) if @search.list
60
60
  end
61
61
 
62
- if dimension < @dimensions.size
63
- xml << render(:partial => '/drilldown/excel_summary_total_row', :locals => { :result => result, :headers => headers.dup, :dimension => dimension })
64
- end
62
+ xml << render(partial: '/drilldown/excel_summary_total_row', locals: { result: result, headers: headers.dup, dimension: dimension }) if dimension < @dimensions.size
65
63
  xml
66
64
  end
67
65
  end
@@ -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
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleDrilldown
4
+ # Routing helper methods
5
+ module Routing
6
+ def draw_drilldown(path, controller = path)
7
+ get "#{path}(.:format)" => "#{controller}#index", as: path
8
+ scope path do
9
+ %i[choices excel_export html_export index].each do |action|
10
+ get "#{action}(/:id)(.:format)", controller: controller, action: action
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,3 +1,3 @@
1
1
  module SimpleDrilldown
2
- VERSION = '0.3.3'
2
+ VERSION = '0.6.0'
3
3
  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.3.3
4
+ version: 0.6.0
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-03-18 00:00:00.000000000 Z
11
+ date: 2020-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chartkick
@@ -90,7 +90,7 @@ files:
90
90
  - app/jobs/simple_drilldown/application_job.rb
91
91
  - app/mailers/simple_drilldown/application_mailer.rb
92
92
  - app/models/simple_drilldown/application_record.rb
93
- - app/views/drilldown/_chart.html.slim
93
+ - app/views/drilldown/_chart.html.erb
94
94
  - app/views/drilldown/_excel_record_list.builder
95
95
  - app/views/drilldown/_excel_row.builder
96
96
  - app/views/drilldown/_excel_row_header.builder
@@ -123,10 +123,12 @@ 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/drilldown_controller.rb
128
+ - lib/simple_drilldown/controller.rb
128
129
  - lib/simple_drilldown/drilldown_helper.rb
129
130
  - lib/simple_drilldown/engine.rb
131
+ - lib/simple_drilldown/routing.rb
130
132
  - lib/simple_drilldown/search.rb
131
133
  - lib/simple_drilldown/version.rb
132
134
  - lib/tasks/simple_drilldown_tasks.rake
@@ -135,7 +137,7 @@ licenses:
135
137
  - MIT
136
138
  metadata:
137
139
  allowed_push_host: https://rubygems.org/
138
- post_install_message:
140
+ post_install_message:
139
141
  rdoc_options: []
140
142
  require_paths:
141
143
  - lib
@@ -151,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
151
153
  version: '0'
152
154
  requirements: []
153
155
  rubygems_version: 3.1.2
154
- signing_key:
156
+ signing_key:
155
157
  specification_version: 4
156
158
  summary: Simple data warehouse and drilldown.
157
159
  test_files: []
@@ -1,52 +0,0 @@
1
- ruby:
2
- data =
3
- case @dimensions.size
4
- when 0
5
- { @result[:value] => @result[:count] }
6
- when 1
7
- @result[:rows].map { |r| [ @dimensions[0][:label_method] ? @dimensions[0][:label_method].call(r[:value]) : r[:value], r[:count] ] }
8
- when 2
9
- @result[:rows].map do |r|
10
- {
11
- name: r[:value],
12
- data: r[:rows].map { |r2| [r2[:value], r2[:count]] }
13
- }
14
- end
15
- when 3
16
- end
17
- - case @search.display_type
18
- - when SimpleDrilldown::Search::DisplayType::PIE
19
- = pie_chart data, height: '24rem'
20
- - when SimpleDrilldown::Search::DisplayType::BAR
21
- = column_chart data, height: '24rem'
22
- - when SimpleDrilldown::Search::DisplayType::LINE
23
- = line_chart data, height: '24rem'
24
- - else
25
- #drilldown_area
26
- h2 = caption
27
- h3 = subcaption
28
- br
29
- #drilldown_search_area style="margin-left: auto; margin-right: auto; text-align: center"
30
- - (0..2).each do |i|
31
- - options = [['', '']]
32
- - options << [@dimensions[i][:pretty_name], @dimensions[i][:url_param_name]] if @dimensions[i]
33
- - options += @remaining_dimensions.keys.map { |name| [@dimension_defs[name][:pretty_name], name] }
34
- | #{t(i == 0 ? :group_by : :then_by)}:
35
- = form.select 'dimensions', options, { :selected => @search.dimensions && @search.dimensions[i] }, \
36
- { :onChange => 'form.submit()', :name => 'search[dimensions][]', :id => "search_dimensions_#{i}" }
37
- br
38
- = t :chart_type
39
- = form.radio_button 'display_type', SimpleDrilldown::Search::DisplayType::BAR, { :onChange => 'form.submit()' }
40
- = form.label :display_type_bar, t(:bar)
41
- = form.radio_button 'display_type', SimpleDrilldown::Search::DisplayType::PIE, { :disabled => @search.dimensions.size >= 2, :onChange => 'form.submit()' }
42
- = form.label :display_type_pie, t(:pie)
43
- = form.radio_button 'display_type', SimpleDrilldown::Search::DisplayType::LINE, { :onChange => 'form.submit()' }
44
- = form.label :display_type_line, t(:line)
45
- = form.radio_button 'display_type', SimpleDrilldown::Search::DisplayType::NONE, { :onChange => 'form.submit()' }
46
- = form.label :display_type_none, t(:none)
47
-
48
- = form.check_box :order_by_value, { :onChange => 'form.submit()' }
49
- = form.label :order_by_value, t(:order_by_value)
50
-
51
- = form.check_box :list, { :onChange => 'form.submit()' }
52
- = form.label :list, t(:list)