simple_drilldown 0.1.1 → 0.2.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: 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