compendium 1.1.3.4 → 1.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.
Files changed (44) hide show
  1. checksums.yaml +5 -13
  2. data/.travis.yml +12 -0
  3. data/CHANGELOG.md +18 -0
  4. data/Gemfile +4 -0
  5. data/README.md +185 -4
  6. data/app/assets/stylesheets/compendium/options.css.scss +2 -3
  7. data/app/classes/compendium/presenters/chart.rb +1 -1
  8. data/app/classes/compendium/presenters/csv.rb +32 -0
  9. data/app/classes/compendium/presenters/query.rb +4 -2
  10. data/app/classes/compendium/presenters/settings/query.rb +10 -2
  11. data/app/classes/compendium/presenters/settings/table.rb +24 -4
  12. data/app/classes/compendium/presenters/table.rb +33 -18
  13. data/app/controllers/compendium/reports_controller.rb +28 -7
  14. data/app/views/compendium/reports/setup.haml +16 -2
  15. data/compendium.gemspec +4 -3
  16. data/config/locales/en.yml +6 -1
  17. data/config/locales/es.yml +11 -0
  18. data/config/locales/fr.yml +11 -0
  19. data/lib/compendium.rb +1 -0
  20. data/lib/compendium/collection_query.rb +1 -1
  21. data/lib/compendium/count_query.rb +7 -1
  22. data/lib/compendium/dsl.rb +45 -4
  23. data/lib/compendium/engine/mount.rb +13 -5
  24. data/lib/compendium/errors.rb +6 -0
  25. data/lib/compendium/metric.rb +1 -1
  26. data/lib/compendium/open_hash.rb +1 -1
  27. data/lib/compendium/param_types.rb +2 -2
  28. data/lib/compendium/params.rb +1 -1
  29. data/lib/compendium/query.rb +23 -4
  30. data/lib/compendium/report.rb +14 -10
  31. data/lib/compendium/sum_query.rb +3 -1
  32. data/lib/compendium/through_query.rb +7 -2
  33. data/lib/compendium/version.rb +1 -1
  34. data/spec/count_query_spec.rb +41 -5
  35. data/spec/dsl_spec.rb +77 -2
  36. data/spec/presenters/csv_spec.rb +30 -0
  37. data/spec/presenters/settings/query_spec.rb +26 -0
  38. data/spec/presenters/settings/table_spec.rb +64 -0
  39. data/spec/presenters/table_spec.rb +83 -0
  40. data/spec/query_spec.rb +55 -8
  41. data/spec/report_spec.rb +24 -1
  42. data/spec/sum_query_spec.rb +40 -5
  43. metadata +73 -30
  44. data/config/initializers/ruby/hash.rb +0 -6
@@ -15,7 +15,7 @@ module Compendium
15
15
 
16
16
  class << self
17
17
  delegate :validate, to: :params_class
18
-
18
+
19
19
  def inherited(report)
20
20
  Compendium.reports << report
21
21
 
@@ -45,7 +45,7 @@ module Compendium
45
45
  prefix = name.to_s.gsub(/[?!]\z/, '')
46
46
  report_class = "#{prefix}_report".classify.constantize rescue nil
47
47
 
48
- return self == report_class if name.to_s.end_with?('?') and Compendium.reports.include?(report_class)
48
+ return self == report_class if name.to_s.end_with?('?') && Compendium.reports.include?(report_class)
49
49
 
50
50
  super
51
51
  end
@@ -54,7 +54,7 @@ module Compendium
54
54
  prefix = name.to_s.gsub(/[?!]\z/, '')
55
55
  report_class = "#{prefix}_report".classify.constantize rescue nil
56
56
 
57
- return true if name.to_s.end_with?('?') and Compendium.reports.include?(report_class)
57
+ return true if name.to_s.end_with?('?') && Compendium.reports.include?(report_class)
58
58
  super
59
59
  end
60
60
 
@@ -82,10 +82,10 @@ module Compendium
82
82
  self.context = context
83
83
  self.results = {}
84
84
 
85
- only = [options.delete(:only)].flatten.compact
86
- except = [options.delete(:except)].flatten.compact
85
+ only = Array.wrap(options.delete(:only)).compact
86
+ except = Array.wrap(options.delete(:except)).compact
87
87
 
88
- raise ArgumentError, 'cannot specify only and except options at the same time' if !only.empty? and !except.empty?
88
+ raise ArgumentError, 'cannot specify only and except options at the same time' if !only.empty? && !except.empty?
89
89
  (only + except).flatten.each { |q| raise ArgumentError, "invalid query #{q}" unless queries.include?(q) }
90
90
 
91
91
  queries_to_run = if !only.empty?
@@ -105,6 +105,10 @@ module Compendium
105
105
  Collection[Metric, queries.map{ |q| q.metrics.to_a }.flatten]
106
106
  end
107
107
 
108
+ def exports?(type)
109
+ return exporters[type.to_sym]
110
+ end
111
+
108
112
  private
109
113
 
110
114
  attr_accessor :context
@@ -113,17 +117,17 @@ module Compendium
113
117
  prefix = name.to_s.sub(/(?:_results|\?)\Z/, '').to_sym
114
118
 
115
119
  return queries[name] if queries.keys.include?(name)
116
- return results[prefix] if name.to_s.end_with? '_results' and queries.keys.include?(prefix)
120
+ return results[prefix] if name.to_s.end_with?('_results') && queries.keys.include?(prefix)
117
121
  return params[name] if options.keys.include?(name)
118
- return !!params[prefix] if name.to_s.end_with? '?' and options.keys.include?(prefix)
122
+ return !!params[prefix] if name.to_s.end_with?('?') && options.keys.include?(prefix)
119
123
  super
120
124
  end
121
125
 
122
126
  def respond_to_missing?(name, include_private = false)
123
127
  prefix = name.to_s.sub(/_results\Z/, '').to_sym
124
128
  return true if queries.keys.include?(name)
125
- return true if name.to_s.end_with? '_results' and queries.keys.include?(prefix)
129
+ return true if name.to_s.end_with?('_results') && queries.keys.include?(prefix)
126
130
  super
127
131
  end
128
132
  end
129
- end
133
+ end
@@ -1,3 +1,4 @@
1
+ require 'compendium/errors'
1
2
  require 'compendium/query'
2
3
 
3
4
  module Compendium
@@ -5,7 +6,6 @@ module Compendium
5
6
  # Often useful in conjunction with a grouped query and counter cache
6
7
  # (alternately, see CountQuery)
7
8
  class SumQuery < Query
8
- InvalidCommand = Class.new(StandardError)
9
9
 
10
10
  attr_accessor :column
11
11
 
@@ -13,6 +13,8 @@ module Compendium
13
13
  @report = args.shift if arg_is_report?(args.first)
14
14
  @column = args.slice!(1)
15
15
  super(*args)
16
+
17
+ @options.reverse_merge!(order: "SUM(#{@column})", reverse: true)
16
18
  end
17
19
 
18
20
  private
@@ -17,7 +17,7 @@ module Compendium
17
17
 
18
18
  # If none of the through queries have any results, we shouldn't try to execute the query, because it
19
19
  # depends on the results of its parents.
20
- return @results = ResultSet.new([]) if results.compact.empty?
20
+ return @results = ResultSet.new([]) if any_results?(results)
21
21
 
22
22
  # If the proc collects two arguments, pass results and params, otherwise just results
23
23
  args = !proc || proc.arity == 1 ? [results] : [results, params]
@@ -32,7 +32,7 @@ module Compendium
32
32
  def collect_through_query_results(params, context)
33
33
  results = {}
34
34
 
35
- queries = [through].flatten.map(&method(:get_associated_query))
35
+ queries = Array.wrap(through).map(&method(:get_associated_query))
36
36
 
37
37
  queries.each do |q|
38
38
  q.run(params, context) unless q.ran?
@@ -42,5 +42,10 @@ module Compendium
42
42
  results = results[queries.first.name] if queries.size == 1
43
43
  results
44
44
  end
45
+
46
+ def any_results?(results)
47
+ results = results.values if results.is_a? Hash
48
+ results.all?(&:blank?)
49
+ end
45
50
  end
46
51
  end
@@ -1,3 +1,3 @@
1
1
  module Compendium
2
- VERSION = "1.1.3.4"
2
+ VERSION = '1.2.0'
3
3
  end
@@ -1,4 +1,5 @@
1
1
  require 'spec_helper'
2
+ require 'compendium'
2
3
  require 'compendium/count_query'
3
4
 
4
5
  class SingleCounter
@@ -8,14 +9,37 @@ class SingleCounter
8
9
  end
9
10
 
10
11
  class MultipleCounter
12
+ def order(*)
13
+ @order = true
14
+ self
15
+ end
16
+
17
+ def reverse_order
18
+ @reverse = true
19
+ self
20
+ end
21
+
11
22
  def count
12
- { 1 => 123, 2 => 456, 3 => 789 }
23
+ results = { 1 => 340, 2 => 204, 3 => 983 }
24
+
25
+ if @order
26
+ results = results.sort_by{ |r| r[1] }
27
+ results.reverse! if @reverse
28
+ results = Hash[results]
29
+ end
30
+
31
+ results
13
32
  end
14
33
  end
15
34
 
16
35
  describe Compendium::CountQuery do
17
36
  subject { described_class.new(:counted_query, { count: true }, -> * { @counter }) }
18
37
 
38
+ it 'should have a default order' do
39
+ subject.options[:order].should == 'COUNT(*)'
40
+ subject.options[:reverse].should == true
41
+ end
42
+
19
43
  describe "#run" do
20
44
  it "should call count on the proc result" do
21
45
  @counter = SingleCounter.new
@@ -28,14 +52,26 @@ describe Compendium::CountQuery do
28
52
  subject.run(nil, self).should == [1792]
29
53
  end
30
54
 
31
- it "should return a hash if given" do
32
- @counter = MultipleCounter.new
33
- subject.run(nil, self).should == { 1 => 123, 2 => 456, 3 => 789 }
55
+ context 'when given a hash' do
56
+ before { @counter = MultipleCounter.new }
57
+
58
+ it "should return a hash" do
59
+ subject.run(nil, self).should == { 3 => 983, 1 => 340, 2 => 204 }
60
+ end
61
+
62
+ it 'should be ordered in descending order' do
63
+ subject.run(nil, self).keys.should == [3, 1, 2]
64
+ end
65
+
66
+ it 'should use the given options' do
67
+ subject.options[:reverse] = false
68
+ subject.run(nil, self).keys.should == [2, 1, 3]
69
+ end
34
70
  end
35
71
 
36
72
  it "should raise an error if the proc does not respond to count" do
37
73
  @counter = Class.new
38
- expect { subject.run(nil, self) }.to raise_error Compendium::CountQuery::InvalidCommand
74
+ expect { subject.run(nil, self) }.to raise_error Compendium::InvalidCommand
39
75
  end
40
76
  end
41
77
  end
data/spec/dsl_spec.rb CHANGED
@@ -46,9 +46,14 @@ describe Compendium::DSL do
46
46
  end
47
47
 
48
48
  describe "#query" do
49
+ let(:proc1) { -> { :proc1 } }
50
+ let(:proc2) { -> { :proc2 } }
51
+
49
52
  let(:report_class) do
53
+ proc1 = proc1
54
+
50
55
  Class.new(Compendium::Report) do
51
- query :test
56
+ query :test, &proc1
52
57
  end
53
58
  end
54
59
 
@@ -65,6 +70,36 @@ describe Compendium::DSL do
65
70
  subject.test.report.should == subject
66
71
  end
67
72
 
73
+ context 'when overriding an existing query' do
74
+ before do
75
+ subject.query :test, &proc2
76
+ subject.query :another_test, count: true
77
+ end
78
+
79
+ it 'should delete the existing query' do
80
+ subject.queries.count.should == 2
81
+ end
82
+
83
+ it 'should only have one query with each name' do
84
+ subject.queries.map(&:name).should =~ [:test, :another_test]
85
+ end
86
+
87
+ it 'should use the new proc' do
88
+ subject.test.proc.should == proc2
89
+ end
90
+
91
+ it 'should not allow replacing a query with a different type' do
92
+ expect { subject.query :test, count: true }.to raise_error { Compendium::CannotRedefineQueryType }
93
+ subject.test.should be_instance_of Compendium::Query
94
+ end
95
+
96
+ it 'should allow replacing a query with the same type' do
97
+ subject.query :another_test, count: true, &proc2
98
+ subject.another_test.proc.should == proc2
99
+ subject.another_test.should be_instance_of Compendium::CountQuery
100
+ end
101
+ end
102
+
68
103
  context "when given a through option" do
69
104
  before { report_class.query :through, through: :test }
70
105
  subject { report_class.queries[:through] }
@@ -221,6 +256,46 @@ describe Compendium::DSL do
221
256
  end
222
257
  end
223
258
 
259
+ describe '#table' do
260
+ let(:table_proc) { -> { display_nil_as 'na' } }
261
+
262
+ it 'should add table settings to the given query' do
263
+ subject.query :test
264
+ subject.table :test, &table_proc
265
+ subject.queries[:test].table_settings.should == table_proc
266
+ end
267
+
268
+ it 'should raise an error if there is no query of the given name' do
269
+ expect { subject.table :test, &table_proc }.to raise_error(ArgumentError, "query test is not defined")
270
+ end
271
+
272
+ it 'should allow table settings to be applied to multiple queries at once' do
273
+ subject.query :query1
274
+ subject.query :query2
275
+ subject.table :query1, :query2, &table_proc
276
+ subject.queries[:query1].table_settings.should == table_proc
277
+ subject.queries[:query2].table_settings.should == table_proc
278
+ end
279
+ end
280
+
281
+ describe '#exports' do
282
+ it 'should not have any exporters by default' do
283
+ subject.exporters.should be_empty
284
+ end
285
+
286
+ it 'should set the export to true if no options are given' do
287
+ subject.exports :csv
288
+ subject.exporters[:csv].should be_true
289
+ end
290
+
291
+ it 'should save any given options' do
292
+ subject.exports :csv, :main_query
293
+ subject.exports :pdf, :foo, :bar
294
+ subject.exporters[:csv].should == :main_query
295
+ subject.exporters[:pdf].should == [:foo, :bar]
296
+ end
297
+ end
298
+
224
299
  it "should allow previously defined queries to be redefined by name" do
225
300
  subject.query :test_query
226
301
  subject.test_query foo: :bar
@@ -231,4 +306,4 @@ describe Compendium::DSL do
231
306
  subject.query :test_query
232
307
  subject.test_query.should == subject.queries[:test_query]
233
308
  end
234
- end
309
+ end
@@ -0,0 +1,30 @@
1
+ require 'compendium/presenters/csv'
2
+
3
+ describe Compendium::Presenters::CSV do
4
+ let(:results) { double('Results', records: [{ group: 'A', one: 1, two: 2 }, { group: 'B', one: 3, two: 4 }], keys: [:group, :one, :two]) }
5
+ let(:query) { double('Query', results: results, options: {}, table_settings: nil) }
6
+ let(:presenter) { described_class.new(query) }
7
+
8
+ before do
9
+ query.stub(:pos) { raise caller.join("\n") }
10
+ end
11
+
12
+ before { I18n.stub(:t) { |key| key } }
13
+
14
+ describe '#render' do
15
+ it 'should return a CSV of the results' do
16
+ presenter.render.should == "group,one,two\nA,1.00,2.00\nB,3.00,4.00\n"
17
+ end
18
+
19
+ it "should use the query's table settings" do
20
+ query.stub(:table_settings).and_return(-> * { number_format '%0.0f' })
21
+ presenter.render.should == "group,one,two\nA,1,2\nB,3,4\n"
22
+ end
23
+
24
+ it 'should output a total row if the query has totals' do
25
+ query.results.records << { group: '', one: 4, two: 6 }
26
+ query.options[:totals] = true
27
+ presenter.render.should == "group,one,two\nA,1.00,2.00\nB,3.00,4.00\ntotal,4.00,6.00\n"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+ require 'compendium/presenters/settings/query'
3
+
4
+ describe Compendium::Presenters::Settings::Query do
5
+ subject { described_class.new }
6
+
7
+ describe '#update' do
8
+ before { subject.foo = :bar }
9
+
10
+ it 'should override previous settings' do
11
+ subject.update do |s|
12
+ s.foo :quux
13
+ end
14
+
15
+ subject.foo.should == :quux
16
+ end
17
+
18
+ it 'should allow the block parameter to be skipped' do
19
+ subject.update do
20
+ foo :quux
21
+ end
22
+
23
+ subject.foo.should == :quux
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+ require 'compendium/presenters/table'
3
+
4
+ describe Compendium::Presenters::Settings::Table do
5
+ let(:results) { double('Results', records: [{ one: 1, two: 2 }, { one: 3, two: 4 }], keys: [:one, :two]) }
6
+ let(:query) { double('Query', results: results, options: {}, table_settings: nil) }
7
+ let(:table) { Compendium::Presenters::Table.new(nil, query) }
8
+
9
+ subject { table.settings }
10
+
11
+ context 'default settings' do
12
+ its(:number_format) { should == '%0.2f' }
13
+ its(:table_class) { should == 'results' }
14
+ its(:header_class) { should == 'headings' }
15
+ its(:row_class) { should == 'data' }
16
+ its(:totals_class) { should == 'totals' }
17
+ its(:display_nil_as) { should be_nil }
18
+ its(:skipped_total_cols) { should be_empty }
19
+ end
20
+
21
+ context 'overriding default settings' do
22
+ let(:table) do
23
+ Compendium::Presenters::Table.new(nil, query) do |t|
24
+ t.number_format '%0.1f'
25
+ t.table_class 'report_table'
26
+ t.header_class 'report_heading'
27
+ t.display_nil_as 'N/A'
28
+ end
29
+ end
30
+
31
+ its(:number_format) { should == '%0.1f' }
32
+ its(:table_class) { should == 'report_table' }
33
+ its(:header_class) { should == 'report_heading' }
34
+ its(:display_nil_as) { should == 'N/A' }
35
+ end
36
+
37
+ describe '#update' do
38
+ it 'should override previous settings' do
39
+ subject.update do |s|
40
+ s.number_format '%0.3f'
41
+ end
42
+
43
+ subject.number_format.should == '%0.3f'
44
+ end
45
+ end
46
+
47
+ describe '#skip_total_for' do
48
+ it 'should add columns to the setting' do
49
+ subject.skip_total_for :foo, :bar
50
+ subject.skipped_total_cols.should == [:foo, :bar]
51
+ end
52
+
53
+ it 'should be callable multiple times' do
54
+ subject.skip_total_for :foo, :bar
55
+ subject.skip_total_for :quux
56
+ subject.skipped_total_cols.should == [:foo, :bar, :quux]
57
+ end
58
+
59
+ it 'should not care about type' do
60
+ subject.skip_total_for 'foo'
61
+ subject.skipped_total_cols.should == [:foo]
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,83 @@
1
+ require 'spec_helper'
2
+ require 'compendium/presenters/table'
3
+
4
+ describe Compendium::Presenters::Table do
5
+ let(:template) { double('Template') }
6
+ let(:results) { double('Results', records: [{ one: 1, two: 2 }, { one: 3, two: 4 }], keys: [:one, :two]) }
7
+ let(:query) { double('Query', results: results, options: {}, table_settings: nil) }
8
+ let(:table) { described_class.new(template, query) }
9
+
10
+ context 'render' do
11
+ before do
12
+ template.stub(:content_tag) { |element, *, &block| block.nil? ? element : table.instance_exec(&block) }
13
+ I18n.stub(:t) { |key| key }
14
+ end
15
+
16
+ it 'should use the table class given in settings' do
17
+ table.settings.table_class 'report_table'
18
+
19
+ template.should_receive(:content_tag).with(:table, class: 'report_table')
20
+ table.render
21
+ end
22
+
23
+ it 'should use the default table class if not overridden' do
24
+ template.should_receive(:content_tag).with(:table, class: 'results')
25
+ table.render
26
+ end
27
+
28
+ it 'should build the heading row' do
29
+ template.should_receive(:content_tag).with(:tr, class: 'headings')
30
+ template.should_receive(:content_tag).with(:th, :one)
31
+ template.should_receive(:content_tag).with(:th, :two)
32
+ table.render
33
+ end
34
+
35
+ it 'should use the overridden heading class if given' do
36
+ table.settings.header_class 'report_header'
37
+
38
+ template.should_receive(:content_tag).with(:tr, class: 'report_header')
39
+ table.render
40
+ end
41
+
42
+ it 'should build data rows' do
43
+ template.should_receive(:content_tag).with(:tr, class: 'data').twice
44
+ table.render
45
+ end
46
+
47
+ it 'should use the overridden row class if given' do
48
+ table.settings.row_class 'report_row'
49
+
50
+ template.should_receive(:content_tag).with(:tr, class: 'report_row').twice
51
+ table.render
52
+ end
53
+
54
+ it 'should add a totals row if the query has totals: true set' do
55
+ query.options[:totals] = true
56
+
57
+ template.should_receive(:content_tag).with(:tr, class: 'totals')
58
+ table.render
59
+ end
60
+
61
+ it 'should not add a totals row if the query has totals: false set' do
62
+ query.options[:totals] = false
63
+
64
+ template.should_not_receive(:content_tag).with(:tr, class: 'totals')
65
+ table.render
66
+ end
67
+
68
+ it 'should not add a totals row if the query does not have :totals set' do
69
+ query.options.delete(:totals)
70
+
71
+ template.should_not_receive(:content_tag).with(:tr, class: 'totals')
72
+ table.render
73
+ end
74
+
75
+ it 'should use the totals class if that setting is overridden' do
76
+ query.options[:totals] = true
77
+ table.settings.totals_class 'report_totals'
78
+
79
+ template.should_receive(:content_tag).with(:tr, class: 'report_totals')
80
+ table.render
81
+ end
82
+ end
83
+ end