compendium 1.1.3.4 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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