simple_drilldown 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24272122319881a4c28db29d1b986ebd2f49d0ed50d642648fe5e5795c4f04b4
4
- data.tar.gz: c5ec174af7dcb4d1ca5dfabfa0abc27b8999177d2f6580be2f65f874825bfcf0
3
+ metadata.gz: 203d9f2001a9677cd008ef0e7f9a10755420c1a06269d3fe2e37cb7467d35dcc
4
+ data.tar.gz: 18bbbd147c2f78ae45674054c6f5af919f49e792da115971ba145693f2528f67
5
5
  SHA512:
6
- metadata.gz: 6310bab272395d0b003616e99e9467c81f31148a6410e83e124770589ef6e9e81de2b60ca8aa84b31eadfc1fb62bfbe81a7e377187a2d0479d0c6e3c819cdbe1
7
- data.tar.gz: ba2943b101e839e0793c51c4d80e94e27952476993c70b6eeaaa1a3a953336da0115890262f2a135ca49669a6298b8bf777bc5d60ba304616ca3c61041c504a4
6
+ metadata.gz: 81dc94488937c04c7563cbc6560fed331cb8bd92a57751fc2df36372c87fe6e25d206d4a983f34d7f3473786bdb78e452a4058f453cf1fa48ec51b94d2efb3d3
7
+ data.tar.gz: f3fcaed4cadf14219ed36cc683e8ad04fc16c27b80c1b5112fb133cfb8a2060d81b8210953b656c848da2fa63c4989c75f3e02cbd91d2c940e476148d92fb2fb
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class DrilldownControllerGenerator < Rails::Generators::NamedBase
2
4
  source_root File.expand_path('templates', __dir__)
3
5
 
4
6
  def copy_drilldown_controller_file
5
- template "drilldown_controller.rb.erb", "app/controllers/#{file_name}_drilldown_controller.rb"
7
+ template 'drilldown_controller.rb.erb', "app/controllers/#{file_name}_drilldown_controller.rb"
6
8
  route "resources(:#{singular_name}_drilldown, only: :index){collection{get :excel_export;get :html_export}}"
7
9
  end
8
10
  end
@@ -47,26 +47,52 @@ module SimpleDrilldown
47
47
  @@fields[name] = options
48
48
  end
49
49
 
50
- def dimension(name, select_expression = name.to_s, options = {})
51
- includes = options.delete(:includes)
50
+ def summary_fields(*summary_fields)
51
+ @@summary_fields = summary_fields
52
+ end
53
+
54
+ def dimension(name, select_expression = name, options = {})
52
55
  interval = options.delete(:interval)
53
- label_method = options.delete(:label_method) || ->(f) { f.to_s }
56
+ label_method = options.delete(:label_method)
54
57
  legal_values = options.delete(:legal_values) || legal_values_for(name)
55
58
  reverse = options.delete(:reverse)
59
+ row_class = options.delete(:row_class)
60
+ if select_expression.is_a?(Array)
61
+ queries = select_expression
62
+ else
63
+ includes = options.delete(:includes)
64
+ conditions = options.delete(:where)
65
+ queries = [{
66
+ select: select_expression,
67
+ includes: includes,
68
+ where: conditions
69
+ }]
70
+ end
71
+ raise "Unexpected options: #{options.inspect}" if options.present?
56
72
 
57
- raise "Unknown options: #{options.keys.inspect}" unless options.empty?
73
+ queries.each do |query_opts|
74
+ raise "Unknown options: #{query_opts.keys.inspect}" unless (query_opts.keys - %i[select includes where]).empty?
75
+ end
58
76
 
59
77
  @@dimension_defs ||= Concurrent::Hash.new
60
78
 
61
79
  @@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
80
+ includes: queries.inject([]) do |a, e|
81
+ i = e[:includes]
82
+ next a unless i
83
+ next a if a.include?(i)
84
+
85
+ a + [i]
86
+ end,
87
+ interval: interval,
88
+ label_method: label_method,
89
+ legal_values: legal_values,
90
+ pretty_name: I18n.t(name),
91
+ queries: queries,
92
+ reverse: reverse,
93
+ select_expression: queries.size > 1 ? "COALESCE(#{queries.map { |q| q[:select] }.join(',')})" : queries[0][:select],
94
+ row_class: row_class,
95
+ url_param_name: name.to_s
70
96
  }
71
97
  end
72
98
 
@@ -74,29 +100,38 @@ module SimpleDrilldown
74
100
  lambda do |search|
75
101
  my_filter = search.filter.dup
76
102
  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]
103
+ filter_conditions, _t, includes = make_conditions(my_filter)
104
+ dimension_def = @@dimension_defs[field.to_s]
105
+ result_sets = dimension_def[:queries].map do |query|
106
+ if query[:includes]
107
+ if query[:includes].is_a?(Array)
108
+ includes += query[:includes]
109
+ else
110
+ includes << query[:includes]
111
+ end
112
+ includes.uniq!
84
113
  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 }
114
+ rows = @@target_class.unscoped.where(@@base_condition)
115
+ .select("#{query[:select]} AS value")
116
+ .where(filter_conditions)
117
+ .where(query[:where])
118
+ .joins(make_join([], @@target_class.name.underscore.to_sym, includes))
119
+ .order('value')
120
+ .group(:value)
121
+ .to_a
122
+ filter_fields = search.filter[field.to_s]
123
+ filter_fields&.each do |selected_value|
124
+ next if rows.find { |r| r[:value].to_s == selected_value }
125
+
126
+ # FIXME(uwe): Convert the selected value to same data type as legal values
95
127
  rows << { value: selected_value }
128
+ # EMXIF
96
129
  end
130
+ rows.map { |r| [dimension_def[:label_method]&.call(r[:value]) || r[:value], r[:value]] }
97
131
  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]
132
+ values = result_sets.inject(&:+).uniq
133
+ values.sort! if dimension_def[:queries].size > 1
134
+ values.reverse! if dimension_def[:reverse]
100
135
  values
101
136
  end
102
137
  end
@@ -115,22 +150,24 @@ module SimpleDrilldown
115
150
  values = [*values]
116
151
  if dimension_def[:interval]
117
152
  values *= 2 if values.size == 1
118
- if values.size != 2
119
- raise "Need 2 values for interval filter: #{values.inspect}"
120
- end
153
+ raise "Need 2 values for interval filter: #{values.inspect}" if values.size != 2
121
154
 
122
- if !values[0].blank? && !values[1].blank?
155
+ if values[0].present? && values[1].present?
123
156
  condition_strings << "#{dimension_def[:select_expression]} BETWEEN ? AND ?"
124
157
  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?
158
+ filter_texts << <<~TEXT
159
+ #{dimension_def[:pretty_name]} #{dimension_def[:label_method]&.call(values) || "from #{values[0]} to #{values[1]}"}
160
+ TEXT
161
+ elsif values[0].present?
127
162
  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?
163
+ condition_values << values[0]
164
+ filter_texts <<
165
+ "#{dimension_def[:pretty_name]} #{dimension_def[:label_method]&.call(values) || "from #{values[0]}"}"
166
+ elsif values[1].present?
131
167
  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]}"}"
168
+ condition_values << values[1]
169
+ filter_texts <<
170
+ "#{dimension_def[:pretty_name]} #{dimension_def[:label_method]&.call(values) || "to #{values[1]}"}"
134
171
  end
135
172
  includes << dimension_def[:includes] if dimension_def[:includes]
136
173
  else
@@ -140,7 +177,8 @@ module SimpleDrilldown
140
177
  else
141
178
  condition_values << value
142
179
  end
143
- filter_texts << "#{dimension_def[:pretty_name]} #{dimension_def[:label_method] ? dimension_def[:label_method].call(value) : value}"
180
+ filter_texts <<
181
+ "#{dimension_def[:pretty_name]} #{dimension_def[:label_method]&.call(value) || value}"
144
182
  includes << dimension_def[:includes] if dimension_def[:includes]
145
183
  "(#{dimension_def[:select_expression]}) = ?"
146
184
  end.join(' OR ')
@@ -148,11 +186,12 @@ module SimpleDrilldown
148
186
  end
149
187
  filter_text = filter_texts.join(' and ')
150
188
  conditions = [condition_strings.map { |c| "(#{c})" }.join(' AND '), *condition_values]
151
- includes.uniq!
189
+ includes.keep_if(&:present?).uniq!
152
190
  else
153
191
  filter_text = nil
154
192
  conditions = nil
155
193
  end
194
+ conditions = nil if conditions == ['']
156
195
  [conditions, filter_text, includes]
157
196
  end
158
197
 
@@ -161,7 +200,7 @@ module SimpleDrilldown
161
200
  when Array
162
201
  include.map { |i| make_join(joins, model, i) }.join(' ')
163
202
  when Hash
164
- sql = ''
203
+ sql = +''
165
204
  include.each do |parent, child|
166
205
  sql << make_join(joins, model, parent) + ' '
167
206
  ass = model.to_s.camelize.constantize.reflect_on_association parent
@@ -171,6 +210,7 @@ module SimpleDrilldown
171
210
  when Symbol
172
211
  return '' if joins.include?(include)
173
212
 
213
+ joins = joins.dup
174
214
  joins << include
175
215
  ass = model_class.reflect_on_association include
176
216
  raise "Unknown association: #{model} => #{include}" unless ass
@@ -183,17 +223,20 @@ module SimpleDrilldown
183
223
  "LEFT JOIN #{include_table} #{include_alias} ON #{include_alias}.id = #{model_table}.#{include}_id"
184
224
  when :has_one, :has_many
185
225
  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, '')
226
+ sql = +"LEFT JOIN #{include_table} #{include_alias} ON #{include_alias}.#{fk_col} = #{model_table}.id"
227
+ sql << " AND #{include_alias}.deleted_at IS NULL" if ass.klass.paranoid?
228
+ if ass.scope && (ass_order = ScopeHolder.new(ass.scope).to_s)
229
+ ass_order = ass_order.sub(/ DESC\s*$/i, '')
189
230
  ass_order_prefixed = ass_order.dup
190
231
  ActiveRecord::Base.connection.columns(include_table).map(&:name).each do |cname|
191
232
  ass_order_prefixed.gsub!(/\b#{cname}\b/, "#{include_alias}.#{cname}")
192
233
  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})"
234
+ paranoid_clause = 'AND t2.deleted_at IS NULL' if ass.klass.paranoid?
235
+ # FIXME(uwe): Should we add "where" from the ScopeHolder here as well? Ref: DrilldownChanges#changes_for
236
+ min_query = <<~SQL
237
+ SELECT MIN(#{ass_order}) FROM #{include_table} t2 WHERE t2.#{fk_col} = #{model_table}.id #{paranoid_clause}
238
+ SQL
239
+ sql << " AND #{ass_order_prefixed} = (#{min_query})"
197
240
  end
198
241
  sql
199
242
  else
@@ -225,13 +268,16 @@ module SimpleDrilldown
225
268
  @list_includes = @@list_includes
226
269
  @list_order = @@list_order
227
270
  @dimension_defs = @@dimension_defs
228
- @summary_fields = []
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 }
229
275
  end
230
276
 
231
277
  # ?dimension[0]=supplier&dimension[1]=transaction_type&
232
278
  # filter[year]=2009&filter[supplier][0]=Shell&filter[supplier][1]=Statoil
233
279
  def index(do_render = true)
234
- @search = Search.new(params[:search]&.to_unsafe_h, @default_fields, @default_select_value)
280
+ @search = new_search_object
235
281
 
236
282
  @transaction_fields = (@search.fields + (@fields.keys.map(&:to_s) - @search.fields))
237
283
  @transaction_fields_map = @fields
@@ -240,11 +286,9 @@ module SimpleDrilldown
240
286
  includes = @base_includes.dup
241
287
 
242
288
  @dimensions = []
243
- select << ", 'All' as value0"
289
+ select << ", 'All'::text as value0"
244
290
  @dimensions += @search.dimensions.map do |dn|
245
- if @dimension_defs[dn].nil?
246
- raise "Unknown distribution field: #{@search.dimensions.inspect}"
247
- end
291
+ raise "Unknown distribution field: #{dn.inspect}" if @dimension_defs[dn].nil?
248
292
 
249
293
  @dimension_defs[dn]
250
294
  end
@@ -255,42 +299,108 @@ module SimpleDrilldown
255
299
 
256
300
  conditions, @filter_text, filter_includes = self.class.make_conditions(@search.filter)
257
301
  includes += filter_includes
258
- includes.uniq!
302
+ includes.keep_if(&:present?).uniq!
259
303
  if @search.order_by_value && @dimensions.size <= 1
260
- order = 'count DESC'
304
+ order = case @search.select_value
305
+ when DrilldownSearch::SelectValue::VOLUME
306
+ 'volume DESC'
307
+ when DrilldownSearch::SelectValue::VOLUME_COMPENSATED
308
+ 'volume_compensated DESC'
309
+ when DrilldownSearch::SelectValue::COUNT
310
+ 'count DESC'
311
+ else
312
+ 'count DESC'
313
+ end
261
314
  else
262
- order = @dimensions.map { |d| d[:select_expression] }.join(', ')
315
+ order = (1..@dimensions.size).map { |i| "value#{i}" }.join(',')
263
316
  order = nil if order.empty?
264
317
  end
265
- group = (@base_group + @dimensions.map { |d| d[:select_expression] }).join(', ')
318
+ group = (@base_group + (1..@dimensions.size).map { |i| "value#{i}" }).join(',')
266
319
  group = nil if group.empty?
267
320
 
321
+ joins = self.class.make_join([], @target_class.name.underscore.to_sym, includes)
268
322
  rows = @target_class.unscoped.where(@base_condition).select(select).where(conditions)
269
- .joins(self.class.make_join([], @target_class.name.underscore.to_sym, includes))
323
+ .joins(joins)
270
324
  .group(group)
271
- .order(order).all.to_a
325
+ .order(order).to_a
272
326
 
273
327
  if rows.empty?
274
328
  @result = { value: 'All', count: 0, row_count: 0, nodes: 0, rows: [] }
329
+ @summary_fields.each { |f| @result[f] = 0 }
275
330
  else
276
331
  if do_render && @search.list && rows.inject(0) { |sum, r| sum + r[:count].to_i } > LIST_LIMIT
277
332
  @search.list = false
278
- flash[:notice] = "More than #{LIST_LIMIT} records. List disabled."
333
+ flash.now[:notice] = "More than #{LIST_LIMIT} records. List disabled."
279
334
  end
280
335
  @result = result_from_rows(rows, 0, 0, ['All'])
281
336
  end
282
337
 
283
338
  remove_duplicates(@result) unless @base_group.empty?
284
339
 
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 })
340
+ @remaining_dimensions = @dimension_defs.dup
341
+ @remaining_dimensions.each_key do |dim_name|
342
+ if (@search.filter[dim_name] && @search.filter[dim_name].size == 1) ||
343
+ (@dimensions.any? { |d| d[:url_param_name] == dim_name })
344
+ @remaining_dimensions.delete(dim_name)
345
+ end
288
346
  end
289
347
 
290
348
  populate_list(conditions, includes, @result, []) if @search.list
291
349
  render template: '/drilldown/index' if do_render
292
350
  end
293
351
 
352
+ def choices
353
+ @search = new_search_object
354
+ dimension_name = params[:dimension_name]
355
+ dimension = @dimension_defs[dimension_name]
356
+ selected = @search.filter[dimension_name] || []
357
+ raise "Unknown dimension #{dimension_name.inspect}: #{@dimension_defs.keys.inspect}" unless dimension
358
+
359
+ choices = [[t(:all), nil]] +
360
+ (dimension[:legal_values]&.call(@search)&.map { |o| o.is_a?(Array) ? o[0..1].map(&:to_s) : o.to_s } || [])
361
+ choices_html = choices.map do |c|
362
+ %(<option value="#{c[1]}"#{' SELECTED' if selected.include?(c[1])}>#{c[0]}</option>)
363
+ end.join("\n")
364
+ render html: choices_html.html_safe
365
+ end
366
+
367
+ def html_export
368
+ index(false)
369
+ render template: '/drilldown/html_export', layout: 'print'
370
+ end
371
+
372
+ def excel_export
373
+ index(false)
374
+ headers['Content-Type'] = 'application/vnd.ms-excel'
375
+ headers['Content-Disposition'] = 'attachment; filename="transactions.xml"'
376
+ headers['Cache-Control'] = ''
377
+ render template: '/drilldown/excel_export', layout: false
378
+ end
379
+
380
+ def excel_export_transactions
381
+ params[:search][:list] = '1'
382
+ index(false)
383
+ @transactions = get_transactions(@result)
384
+ headers['Content-Type'] = 'application/vnd.ms-excel'
385
+ headers['Content-Disposition'] = 'attachment; filename="transactions.xml"'
386
+ render template: '/drilldown/excel_export_transactions', layout: false
387
+ end
388
+
389
+ def xml_export
390
+ params[:search][:list] = '1'
391
+ index(false)
392
+ @transactions = get_transactions(@result)
393
+ headers['Content-Type'] = 'text/xml'
394
+ headers['Content-Disposition'] = 'attachment; filename="transactions.xml"'
395
+ render template: '/drilldown/xml_export', layout: false
396
+ end
397
+
398
+ private
399
+
400
+ def new_search_object
401
+ SimpleDrilldown::Search.new(params[:search].to_unsafe_h, @default_fields, @default_select_value)
402
+ end
403
+
294
404
  def remove_duplicates(result)
295
405
  rows = result[:rows]
296
406
  return 0 unless rows
@@ -301,6 +411,9 @@ module SimpleDrilldown
301
411
  if prev_row
302
412
  if prev_row[:value] == r[:value]
303
413
  prev_row[:count] += r[:count]
414
+ @summary_fields.each do |f|
415
+ prev_row[f] += r[f]
416
+ end
304
417
  prev_row[:row_count] = [prev_row[:row_count], r[:row_count]].max
305
418
  prev_row[:nodes] = [prev_row[:nodes], r[:nodes]].max
306
419
  prev_row[:rows] += r[:rows] if prev_row[:rows] || r[:rows]
@@ -322,7 +435,8 @@ module SimpleDrilldown
322
435
 
323
436
  # Empty summary rows are needed to plot zero points in the charts
324
437
  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] }
438
+ legal_values =
439
+ self.class.legal_values_for(@dimensions[dimension][:url_param_name], true).call(@search).map { |lv| lv[1] }
326
440
  legal_values.reverse! if @dimensions[dimension][:reverse]
327
441
  current_values = result_rows.map { |r| r[:value] }.compact
328
442
  empty_values = legal_values - current_values
@@ -335,9 +449,8 @@ module SimpleDrilldown
335
449
  row_count: 0,
336
450
  nodes: 0
337
451
  }
338
- if dimension < @dimensions.size - 1
339
- sub_result[:rows] = add_zero_results([], dimension + 1)
340
- end
452
+ @summary_fields.each { |f| sub_result[f] = 0 }
453
+ sub_result[:rows] = add_zero_results([], dimension + 1) if dimension < @dimensions.size - 1
341
454
  result_rows << sub_result
342
455
  end
343
456
  result_rows = result_rows.sort_by { |r| legal_values.index(r[:value]) }
@@ -353,12 +466,14 @@ module SimpleDrilldown
353
466
  return nil if values != previous_values
354
467
 
355
468
  if dimension == @dimensions.size
356
- return {
469
+ result = {
357
470
  value: values[-1],
358
471
  count: row[:count].to_i,
359
472
  row_count: 1,
360
473
  nodes: @search.list ? 2 : 1
361
474
  }
475
+ @summary_fields.each { |f| result[f] = row[f].to_i }
476
+ return result
362
477
  end
363
478
 
364
479
  result_rows = []
@@ -373,61 +488,84 @@ module SimpleDrilldown
373
488
 
374
489
  result_rows = add_zero_results(result_rows, dimension)
375
490
 
376
- {
491
+ result = {
377
492
  value: values[-1],
378
493
  count: result_rows.inject(0) { |t, r| t + r[:count].to_i },
379
494
  row_count: result_rows.inject(0) { |t, r| t + r[:row_count] },
380
495
  nodes: result_rows.inject(0) { |t, r| t + r[:nodes] } + 1,
381
496
  rows: result_rows
382
497
  }
498
+ @summary_fields.each { |f| result[f] = result_rows.inject(0) { |t, r| t + r[f] } }
499
+ result
383
500
  end
384
501
 
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
502
  def populate_list(conditions, includes, result, values)
401
503
  if result[:rows]
402
504
  result[:rows].each do |r|
403
505
  populate_list(conditions, includes, r, values + [r[:value]])
404
506
  end
405
507
  else
406
- options = { include: includes + @list_includes, order: @list_order }
508
+ list_includes = includes + @list_includes
407
509
  @search.fields.each do |field|
408
510
  field_def = @transaction_fields_map[field.to_sym]
409
511
  raise "Field definition missing for: #{field.inspect}" unless field_def
410
512
 
411
513
  field_includes = field_def[:include]
412
514
  if field_includes
413
- options[:include] += field_includes.is_a?(Array) ? field_includes : [field_includes]
515
+ list_includes += field_includes.is_a?(Array) ? field_includes : [field_includes]
414
516
  end
415
517
  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.unscoped.joins(joins).where(@base_condition).where(list_conditions(conditions, values)).includes(options[:include]).order(options[:order]).all
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
523
+ 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
420
529
  end
421
530
  end
422
531
 
423
532
  def list_conditions(conditions, values)
533
+ conditions ||= ['']
534
+
424
535
  list_conditions_string = conditions[0].dup
425
536
  @dimensions.each do |d|
426
- list_conditions_string << "#{unless list_conditions_string.empty?
427
- ' AND '
428
- end}#{d[:select_expression]} = ?"
537
+ list_conditions_string << "#{' AND ' unless list_conditions_string.empty?}#{d[:select_expression]} = ?"
429
538
  end
430
539
  [list_conditions_string, *(conditions[1..-1] + values)]
431
540
  end
541
+
542
+ def get_transactions(tree)
543
+ return tree[:transactions] if tree[:transactions]
544
+
545
+ tree[:rows].map { |r| get_transactions(r) }.flatten
546
+ end
547
+
548
+ class ScopeHolder
549
+ def initialize(scope)
550
+ instance_eval(&scope)
551
+ end
552
+
553
+ def order(order)
554
+ @order = order
555
+ self
556
+ end
557
+
558
+ def where(*_conditions)
559
+ self
560
+ end
561
+
562
+ def to_s
563
+ if @order.is_a?(Hash)
564
+ @order.map { |field, direction| "#{field} #{direction}" }.join(', ')
565
+ else
566
+ @order.to_s
567
+ end
568
+ end
569
+ end
432
570
  end
433
571
  end
@@ -9,7 +9,7 @@ module SimpleDrilldown
9
9
 
10
10
  def caption
11
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 ') : '')
12
+ ((@dimensions && @dimensions.any?) ? ' by ' + @dimensions.map { |d| d[:pretty_name] }.join(' and ') : '')
13
13
  result.gsub('$date', [*@search.filter[:calendar_date]].uniq.join(' - '))
14
14
  end
15
15
 
@@ -4,8 +4,8 @@ module SimpleDrilldown
4
4
  class Engine < ::Rails::Engine
5
5
  isolate_namespace SimpleDrilldown
6
6
 
7
- initializer "simple_drilldown.assets.precompile" do |app|
8
- app.config.assets.precompile += %w( chartkick.js )
7
+ initializer 'simple_drilldown.assets.precompile' do |app|
8
+ app.config.assets.precompile += %w[chartkick.js]
9
9
  end
10
10
  end
11
11
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_model/naming'
2
4
 
3
5
  module SimpleDrilldown
@@ -14,6 +16,7 @@ module SimpleDrilldown
14
16
  module SelectValue
15
17
  COUNT = 'COUNT'
16
18
  VOLUME = 'VOLUME'
19
+ VOLUME_COMPENSATED = 'VOLUME_COMPENSATED'
17
20
  end
18
21
 
19
22
  attr_reader :dimensions
@@ -22,14 +25,22 @@ module SimpleDrilldown
22
25
  attr_reader :filter
23
26
  attr_accessor :list
24
27
  attr_accessor :percent
25
- attr_reader :last_change_time
28
+ attr_reader :list_change_times
26
29
  attr_reader :order_by_value
27
30
  attr_reader :select_value
28
31
  attr_reader :title
29
32
  attr_reader :default_fields
30
33
 
31
- def initialize(attributes_or_search, default_fields = nil, default_select_value = nil)
32
- if attributes_or_search.is_a? Search
34
+ def self.validators_on(_attribute)
35
+ []
36
+ end
37
+
38
+ def self.human_attribute_name(attribute)
39
+ attribute
40
+ end
41
+
42
+ def initialize(attributes_or_search, default_fields = nil, default_select_value = SelectValue::COUNT)
43
+ if attributes_or_search.is_a? self.class
33
44
  s = attributes_or_search
34
45
  @dimensions = s.dimensions.dup
35
46
  @display_type = s.display_type.dup
@@ -37,7 +48,7 @@ module SimpleDrilldown
37
48
  @filter = s.filter.dup
38
49
  @list = s.list
39
50
  @percent = s.percent
40
- @last_change_time = s.last_change_time
51
+ @list_change_times = s.list_change_times
41
52
  @order_by_value = s.order_by_value
42
53
  @select_value = s.select_value.dup
43
54
  @title = s.title
@@ -45,45 +56,48 @@ module SimpleDrilldown
45
56
  else
46
57
  attributes = attributes_or_search
47
58
  @default_fields = default_fields
59
+ @default_select_value = default_select_value
48
60
  @dimensions = attributes && attributes[:dimensions] || []
49
- @dimensions.delete_if { |d| d.empty? }
61
+ @dimensions.delete_if(&:empty?)
50
62
  @filter = attributes && attributes[:filter] ? attributes[:filter] : {}
51
- @filter.each { |k, v| v.delete('') }
52
- @filter.delete_if { |k, v| v.empty? }
53
- @display_type = attributes && attributes[:display_type] ? attributes[:display_type] : DisplayType::NONE
54
- if @dimensions.size >= 2 && @display_type == DisplayType::PIE
55
- @display_type = DisplayType::BAR
63
+ @filter.keys.dup.each { |k| @filter[k] = [*@filter[k]] }
64
+ @filter.each do |_k, v|
65
+ v.delete('')
66
+ v.delete('Select Some Options')
56
67
  end
68
+ @filter.delete_if { |_k, v| v.empty? }
69
+ @display_type = attributes && attributes[:display_type] ? attributes[:display_type] : DisplayType::NONE
70
+ @display_type = DisplayType::BAR if @dimensions.size >= 2 && @display_type == DisplayType::PIE
57
71
 
58
72
  @order_by_value = attributes && (attributes[:order_by_value] == '1')
59
- @select_value = attributes && attributes[:select_value] && attributes[:select_value].size > 0 ? attributes[:select_value] : SelectValue::COUNT
60
- @list = attributes && attributes[:list] && attributes[:list] == '1' || false
73
+ @select_value = attributes&.dig(:select_value).present? ? attributes[:select_value] : @default_select_value
74
+ @list = attributes&.[](:list) == '1'
61
75
  @percent = attributes&.[](:percent) == '1'
62
- @last_change_time = attributes && attributes[:last_change_time] && attributes[:last_change_time] == '1' || false
63
- if (attributes && attributes[:fields])
64
- if attributes[:fields].is_a?(Array)
65
- @fields = attributes[:fields]
66
- else
67
- @fields = attributes[:fields].select { |k, v| v == '1' }.map { |k, v| k }
68
- end
69
- else
70
- @fields = @default_fields
71
- end
72
- @title = attributes[:title] if attributes && attributes[:title] && attributes[:title].size > 0
76
+ @list_change_times = attributes&.[](:list_change_times) == '1'
77
+ @fields = if attributes && attributes[:fields]
78
+ if attributes[:fields].is_a?(Array)
79
+ attributes[:fields]
80
+ else
81
+ attributes[:fields].to_h.select { |_k, v| v == '1' }.map { |k, _v| k }
82
+ end
83
+ else
84
+ @default_fields
85
+ end
86
+ @title = attributes[:title] if attributes&.dig(:title).present?
73
87
  end
74
88
  end
75
89
 
76
90
  def url_options
77
91
  o = {
78
- :search => {
79
- :title => title,
80
- :list => list ? '1' : '0',
81
- :last_change_time => last_change_time ? '1' : '0',
82
- :filter => filter,
92
+ search: {
93
+ title: title,
94
+ list: list ? '1' : '0',
83
95
  percent: percent ? '1' : '0',
84
- :dimensions => dimensions,
85
- :display_type => display_type,
86
- }
96
+ list_change_times: list_change_times ? '1' : '0',
97
+ filter: filter,
98
+ dimensions: dimensions,
99
+ display_type: display_type,
100
+ },
87
101
  }
88
102
  o[:search][:fields] = fields unless fields == @default_fields
89
103
  o
@@ -95,8 +109,9 @@ module SimpleDrilldown
95
109
  end
96
110
 
97
111
  def drill_down(dimensions, *values)
98
- raise "Too many values" if values.size > self.dimensions.size
99
- s = Search.new(self)
112
+ raise 'Too many values' if values.size > self.dimensions.size
113
+
114
+ s = self.class.new(self)
100
115
  values.each_with_index { |v, i| s.filter[dimensions[i][:url_param_name]] = [v] }
101
116
  values.size.times { s.dimensions.shift }
102
117
  s
@@ -1,3 +1,3 @@
1
1
  module SimpleDrilldown
2
- VERSION = '0.1.1'
2
+ VERSION = '0.2.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_drilldown
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uwe Kubosch
@@ -10,6 +10,20 @@ bindir: bin
10
10
  cert_chain: []
11
11
  date: 2020-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: chartkick
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.3'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rails
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -30,20 +44,6 @@ dependencies:
30
44
  - - "<"
31
45
  - !ruby/object:Gem::Version
32
46
  version: '7'
33
- - !ruby/object:Gem::Dependency
34
- name: chartkick
35
- requirement: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '3.3'
40
- type: :runtime
41
- prerelease: false
42
- version_requirements: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - "~>"
45
- - !ruby/object:Gem::Version
46
- version: '3.3'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rubocop
49
49
  requirement: !ruby/object:Gem::Requirement