compendium 1.0.7 → 1.1.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,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- Y2ZlZDg2ZDkxZjRlNjc5Yzk0OTZlYWUyYTJkNDZmMTBkNTlmNDczMA==
4
+ YzRhZmU5NjE5ZTIzMGZmM2JmMzhlNDRhNjY3NTQ2ZjRjYzI0ZDkxNg==
5
5
  data.tar.gz: !binary |-
6
- OWE2NmE5MGZlZjhmNzdlOTljZWQyYmQ4NDU2NzhlZmM5MzZhMjgxNg==
6
+ ODk2NGYxNjU1YzgzZTlkZmM5MmU1ZjljZjNlNjdkZGE1YTY0ZmY1Ng==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- ZjNkMDgzYWI0OGNkZTY4NGJmMDQxMzRjMzcyODEyOWE2MGFhZDlmOWIzMzZl
10
- ZTQ3OGViNWQ3MGY3ZTlkOGFkMzZhNmVjNmM3NmFiMjYwMzc2MmE0YTQ2ZjBm
11
- YzY0ODFkNzlhMzBhZTEzNDkzNTQ3OTVlZmMwZDJiOWE3NWFiYWU=
9
+ OThmYTFhNmQ2MWY0NTdmYmQxYzg2OTkzYzViNThmMTNjNzhhNDgzMjU4NmQ2
10
+ NjYyMThlMzQyMDY4NzU1YWY0MTdiNGIyYWFjNzdlMzgzMGMyZjY0MTk0MjU2
11
+ MzEyYTVjMGE1ZTQ2NTczN2YyOWM5ZTg1YTk1Mzg3ZjYxOTNjOGI=
12
12
  data.tar.gz: !binary |-
13
- ZDk4ODU0OTJmMTU4ODA2ODViZjhlNDVjZTI2YTdlMjA4ZDY2ZGFhZWI1OWQy
14
- ZjM4MjAxNTI2NzRkYTYxNTlhNDAxNzkxYzMwNjdhNDA4MGI3N2NhZWRlOTFm
15
- Yjc1OWQzMzVjNWZhN2QwNGQ3YTZkMTgyZTI0NjRkMmMzNjcyNjI=
13
+ Y2M1NzdhOGQzMWVhZjNmNGVlZTVlY2JiOWQzZTFlZTkzZGY4ZjE5YjdiMWVi
14
+ ZGU2MzY3ZTU4ZTFiNzU4MDQxNTA5ZjBlZTlmY2YyNGIyM2ZlZWQ3MzNhYzMz
15
+ ZjExMWFhOWZhZTIwOGQwZjJlOGQ2ZGE1OTFhYjAxOTA2MzEwMGE=
data/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Change Log
2
+
3
+ ## 1.1.0 (unreleased)
4
+ * Added query filters (allow a result set to be filtered to remove/translate/etc. data)
5
+ * Extract chart providers into their own gems
6
+ * Allow queries to be rendered as charts without having been run yet (to set up for a future AJAX load)
7
+ * Added `Report#url` and `Query#url` methods to get the JSON URL
8
+
9
+ ## 1.0.7
10
+ * Added the ability to render a report or a specific query of a report as JSON
11
+
12
+ ## 1.0.6
13
+ * Added a built-in renderer for metrics
14
+
15
+ ## 1.0.5
16
+ * Fixed the `:only` and `:except` options to `Report#run`
17
+ * Give `ThroughQuery` access to params if the definition block has an arity of 2
18
+ * Fixed mutating results in a `ThroughQuery` block affecting the parent query
data/README.md CHANGED
@@ -24,6 +24,16 @@ class MyReport < Compendium::Report
24
24
  Items.where(delivered: true, purchased_at: (params[:starting_on]..params[:ending_on]))
25
25
  end
26
26
 
27
+ # Define a filter to modify the results from specified query (in this case :deliveries)
28
+ # For example, this can be useful to translate columns prior to rendering, as it will apply
29
+ # for all render types (table, chart, JSON)
30
+ # Note: A filter can be applied to multiple queries at once
31
+ filter :deliveries do |results, params|
32
+ results.each do |row|
33
+ row['price'] = sprintf('$%.2f', row['price'])
34
+ end
35
+ end
36
+
27
37
  # Define a query which collects data by using AR directly
28
38
  query :on_hand_inventory, collect: :active_record do |params|
29
39
  Items.where(in_stock: true)
@@ -3,17 +3,25 @@ require 'active_support/core_ext/array/extract_options'
3
3
 
4
4
  module Compendium::Presenters
5
5
  class Chart < Query
6
- attr_reader :data, :container, :chart_provider
6
+ attr_reader :data, :params, :container, :chart_provider
7
+ attr_accessor :options
7
8
 
8
9
  def initialize(template, object, *args, &setup)
9
- options = args.extract_options!
10
- type, container = args
11
-
12
10
  super(template, object)
13
11
 
14
- @data = options[:index] ? results.records[options[:index]] : results
15
- @data = @data.records if @data.is_a?(Compendium::ResultSet)
16
- @data = @data[0...-1] if query.options[:totals]
12
+ self.options = args.extract_options!
13
+ type, container = args
14
+
15
+ if remote?
16
+ # If the query hasn't run yet, render a chart that loads its data remotely (ie. through AJAX)
17
+ # ie. if rendering a query from a report class directly
18
+ @data = query.url
19
+ @params = collect_params
20
+ else
21
+ @data = options[:index] ? results.records[options[:index]] : results
22
+ @data = @data.records if @data.is_a?(Compendium::ResultSet)
23
+ @data = @data[0...-1] if query.options[:totals]
24
+ end
17
25
 
18
26
  @container = container || query.name
19
27
 
@@ -32,7 +40,34 @@ module Compendium::Presenters
32
40
  end
33
41
 
34
42
  def initialize_chart_provider(type, &setup)
35
- @chart_provider = provider.new(type, @data, &setup)
43
+ @chart_provider = provider.new(type, @data, @params, &setup)
44
+ end
45
+
46
+ def collect_params
47
+ params = {}
48
+ params[:report] = options[:params] if options[:params]
49
+
50
+ if remote? and protected_against_csrf?
51
+ # If we're loading remotely, and CSRF protection is enabled,
52
+ # automatically include the CSRF token in AJAX params
53
+ params.merge!(form_authenticity_param)
54
+ end
55
+
56
+ params
57
+ end
58
+
59
+ # You can force the chart to render remote data, even if the query has already run by passing the remote: true option
60
+ def remote?
61
+ !query.ran? || options.fetch(:remote, false)
62
+ end
63
+
64
+ def protected_against_csrf?
65
+ !@template.controller.forgery_protection_strategy.nil?
66
+ end
67
+
68
+ def form_authenticity_param
69
+ return {} unless protected_against_csrf?
70
+ { @template.controller.request_forgery_protection_token => @template.controller.send(:form_authenticity_token) }
36
71
  end
37
72
  end
38
73
  end
@@ -3,10 +3,17 @@ require 'active_support/core_ext/string/inflections'
3
3
  module Compendium
4
4
  # Abstract wrapper for rendering charts
5
5
  # To add a new chart provider, #initialize and #render must be implemented
6
+ # Custom providers should also override Compendium::AbstractChartProvider.find_chart_provider (but fallback to super)
7
+
6
8
  class AbstractChartProvider
7
9
  attr_reader :chart
8
10
 
9
- def initialize(type, data, &setup_proc)
11
+ # @param type [Symbol] The type of chart you want to render (:pie, :line, etc).
12
+ # Accepted types might vary by provider.
13
+ # @param data_or_url [Enumerable or String] The data or URL to the data you wish to render.
14
+ # Providers may not support loading data remotely.
15
+ # @param params [Hash] If data_or_url is a URL, the params to use for the AJAX request
16
+ def initialize(type, data_or_url, params = {}, &setup_proc)
10
17
  raise NotImplementedError
11
18
  end
12
19
 
@@ -14,17 +21,9 @@ module Compendium
14
21
  raise NotImplementedError
15
22
  end
16
23
 
17
- # As more chart providers are added, this method will have to be extended to find them
24
+ # Chart providers need to override this method to add a hook for themselves
18
25
  def self.find_chart_provider
19
- if defined?(AmCharts)
20
- :AmCharts
21
- else
22
- self.name.demodulize.to_sym
23
- end
26
+ nil
24
27
  end
25
28
  end
26
-
27
- module ChartProvider
28
- autoload :AmCharts, 'compendium/chart_provider/amcharts'
29
- end
30
29
  end
@@ -49,6 +49,13 @@ module Compendium
49
49
  end
50
50
  end
51
51
 
52
+ def filter(*query_names, &block)
53
+ query_names.each do |query_name|
54
+ raise ArgumentError, "query #{query_name} is not defined" unless queries.key?(query_name)
55
+ queries[query_name].add_filter(block)
56
+ end
57
+ end
58
+
52
59
  # Each Report will have its own descendant of Params in order to safely add validations
53
60
  def params_class
54
61
  @params_class ||= Class.new(Params)
@@ -95,6 +102,7 @@ module Compendium
95
102
  end
96
103
 
97
104
  query = query_type.new(*params)
105
+ query.report = self
98
106
 
99
107
  metrics[name] = opts[:metric] if opts.key?(:metric)
100
108
  queries << query
@@ -7,7 +7,7 @@ require_relative '../../config/initializers/ruby/hash'
7
7
 
8
8
  module Compendium
9
9
  class Query
10
- attr_reader :name, :results, :metrics
10
+ attr_reader :name, :results, :metrics, :filters
11
11
  attr_accessor :options, :proc, :report
12
12
 
13
13
  def initialize(*args)
@@ -17,6 +17,7 @@ module Compendium
17
17
 
18
18
  @name, @options, @proc = args
19
19
  @metrics = ::Collection[Metric]
20
+ @filters = ::Collection[Proc]
20
21
  end
21
22
 
22
23
  def initialize_clone(*)
@@ -25,22 +26,40 @@ module Compendium
25
26
  end
26
27
 
27
28
  def run(params, context = self)
28
- collect_results(context, params)
29
- collect_metrics(context)
29
+ if report.is_a?(Class)
30
+ # If running a query directly from a class rather than an instance, the class's query should
31
+ # not be affected/modified, so run the query without a reference back to the report.
32
+ # Otherwise, if the class is subsequently instantiated, the instance will already have results.
33
+ dup.tap{ |q| q.report = nil }.run(params, context)
34
+ else
35
+ collect_results(context, params)
36
+ collect_metrics(context)
30
37
 
31
- @results
38
+ @results
39
+ end
40
+ end
41
+
42
+ # Get a URL for this query (format: :json set by default)
43
+ def url(params = {})
44
+ report.url(params.merge(query: self.name))
32
45
  end
33
46
 
34
47
  def add_metric(name, proc, options = {})
35
48
  Compendium::Metric.new(name, self.name, proc, options).tap { |m| @metrics << m }
36
49
  end
37
50
 
51
+ def add_filter(filter)
52
+ @filters << filter
53
+ end
54
+
38
55
  def render_table(template, *options, &block)
39
56
  Compendium::Presenters::Table.new(template, self, *options, &block).render unless empty?
40
57
  end
41
58
 
42
59
  def render_chart(template, *options, &block)
43
- Compendium::Presenters::Chart.new(template, self, *options, &block).render unless empty?
60
+ # A query can be rendered regardless of if it has data or not
61
+ # Rendering a chart with no result set builds a chart scaffold which can be updated through AJAX
62
+ Compendium::Presenters::Chart.new(template, self, *options, &block).render
44
63
  end
45
64
 
46
65
  def ran?
@@ -55,15 +74,16 @@ module Compendium
55
74
 
56
75
  # A query is empty if it has no results
57
76
  def empty?
58
- results.empty?
77
+ results.blank?
59
78
  end
60
79
 
61
80
  private
62
81
 
63
82
  def collect_results(context, *params)
64
83
  command = context.instance_exec(*params, &proc) if proc
65
- command = fetch_results(command)
66
- @results = ResultSet.new(command) if command
84
+ results = fetch_results(command)
85
+ results = filter_results(results, *params) if filters.any?
86
+ @results = ResultSet.new(results) if results
67
87
  end
68
88
 
69
89
  def collect_metrics(context)
@@ -74,6 +94,20 @@ module Compendium
74
94
  (options.fetch(:collect, nil) == :active_record) ? command : execute_command(command)
75
95
  end
76
96
 
97
+ def filter_results(results, params)
98
+ return unless results
99
+
100
+ filters.each do |f|
101
+ if f.arity == 2
102
+ results = f.call(results, params)
103
+ else
104
+ results = f.call(results)
105
+ end
106
+ end
107
+
108
+ results
109
+ end
110
+
77
111
  def execute_command(command)
78
112
  return [] if command.nil?
79
113
  command = command.to_sql if command.respond_to?(:to_sql)
@@ -11,6 +11,7 @@ module Compendium
11
11
  extend Compendium::DSL
12
12
 
13
13
  delegate :valid?, :errors, to: :params
14
+ delegate :name, :url, to: :class
14
15
 
15
16
  class << self
16
17
  def inherited(report)
@@ -27,6 +28,15 @@ module Compendium
27
28
  }
28
29
  end
29
30
 
31
+ def name
32
+ super.underscore.gsub(/_report$/,'').to_sym
33
+ end
34
+
35
+ # Get a URL for this report (format: :json set by default)
36
+ def url(params = {})
37
+ path_helper(params)
38
+ end
39
+
30
40
  # Define predicate methods for getting the report type
31
41
  # ie. r.spending? checks that r == SpendingReport
32
42
  def method_missing(name, *args, &block)
@@ -45,6 +55,16 @@ module Compendium
45
55
  return true if name.to_s.end_with?('?') and Compendium.reports.include?(report_class)
46
56
  super
47
57
  end
58
+
59
+ private
60
+
61
+ def path_helper(params)
62
+ unless Rails.application.routes.url_helpers.method_defined? :compendium_reports_run_path
63
+ raise ActionController::RoutingError, "compendium_reports_run_path must be defined"
64
+ end
65
+
66
+ Rails.application.routes.url_helpers.compendium_reports_run_path(self.name, params.reverse_merge(format: :json))
67
+ end
48
68
  end
49
69
 
50
70
  def initialize(params = {})
@@ -1,3 +1,3 @@
1
1
  module Compendium
2
- VERSION = "1.0.7"
2
+ VERSION = "1.1.0"
3
3
  end
data/spec/dsl_spec.rb CHANGED
@@ -60,8 +60,8 @@ describe Compendium::DSL do
60
60
  r.test.report.should == r
61
61
  end
62
62
 
63
- it "should not relate a query to the report class" do
64
- subject.test.report.should be_nil
63
+ it "should relate a query to the report class" do
64
+ subject.test.report.should == subject
65
65
  end
66
66
 
67
67
  context "when given a through option" do
@@ -159,6 +159,35 @@ describe Compendium::DSL do
159
159
  end
160
160
  end
161
161
 
162
+ describe "#filter" do
163
+ let(:filter_proc) { ->{ :filter } }
164
+
165
+ it "should add a filter to the given query" do
166
+ subject.query :test
167
+ subject.filter :test, &filter_proc
168
+ subject.queries[:test].filters.should include filter_proc
169
+ end
170
+
171
+ it "should raise an error if there is no query of the given name" do
172
+ expect { subject.filter :test, &filter_proc }.to raise_error(ArgumentError, "query test is not defined")
173
+ end
174
+
175
+ it "should allow multiple filters to be defined for the same query" do
176
+ subject.query :test
177
+ subject.filter :test, &filter_proc
178
+ subject.filter :test, &->{ :another_filter }
179
+ subject.queries[:test].filters.count.should == 2
180
+ end
181
+
182
+ it "should allow a filter to be applied to multiple queries at once" do
183
+ subject.query :query1
184
+ subject.query :query2
185
+ subject.filter :query1, :query2, &filter_proc
186
+ subject.queries[:query1].filters.should include filter_proc
187
+ subject.queries[:query2].filters.should include filter_proc
188
+ end
189
+ end
190
+
162
191
  it "should allow previously defined queries to be redefined by name" do
163
192
  subject.query :test_query
164
193
  subject.test_query foo: :bar
@@ -2,16 +2,16 @@ require 'spec_helper'
2
2
  require 'compendium/presenters/chart'
3
3
 
4
4
  describe Compendium::Presenters::Chart do
5
+ let(:template) { double('Template', forgery_protection_strategy: nil, request_forgery_protection_token: :authenticity_token, form_authenticity_token: "ABCDEFGHIJ").as_null_object }
6
+ let(:query) { double('Query', name: 'test_query', results: results, ran?: true, options: {}).as_null_object }
7
+ let(:results) { Compendium::ResultSet.new([]) }
8
+
5
9
  before do
6
10
  described_class.any_instance.stub(:provider) { double('ChartProvider') }
7
11
  described_class.any_instance.stub(:initialize_chart_provider)
8
12
  end
9
13
 
10
14
  describe '#initialize' do
11
- let(:template) { double('Template') }
12
- let(:query) { double('Query', name: 'test_query', results: results, options: {}) }
13
- let(:results) { Compendium::ResultSet.new([]) }
14
-
15
15
  context 'when all params are given' do
16
16
  subject{ described_class.new(template, query, :pie, :container) }
17
17
 
@@ -33,5 +33,39 @@ describe Compendium::Presenters::Chart do
33
33
  its(:data) { should == results.records[:one] }
34
34
  its(:container) { should == 'test_query' }
35
35
  end
36
+
37
+ context "when the query has not been run" do
38
+ before { query.stub(ran?: false, url: '/path/to/query.json') }
39
+
40
+ subject{ described_class.new(template, query, :pie, params: { foo: 'bar' }) }
41
+
42
+ its(:data) { should == '/path/to/query.json' }
43
+ its(:params) { should == { report: { foo: 'bar' } } }
44
+
45
+ context "when CSRF protection is enabled" do
46
+ before { template.stub(forgery_protection_strategy: double('CSRF')) }
47
+
48
+ its(:params) { should include authenticity_token: "ABCDEFGHIJ" }
49
+ end
50
+
51
+ context "when CSRF protection is disabled" do
52
+ its(:params) { should_not include authenticity_token: "ABCDEFGHIJ" }
53
+ end
54
+ end
55
+ end
56
+
57
+ describe '#remote?' do
58
+ it 'should be true if options[:remote] is set to true' do
59
+ described_class.new(template, query, :pie, remote: true).should be_remote
60
+ end
61
+
62
+ it 'should be true if the query has not been run yet' do
63
+ query.stub(run?: false)
64
+ described_class.new(template, query, :pie).should_be_remote
65
+ end
66
+
67
+ it 'should be false otherwise' do
68
+ described_class.new(template, query, :pie).should_not be_remote
69
+ end
36
70
  end
37
71
  end
data/spec/query_spec.rb CHANGED
@@ -49,6 +49,43 @@ describe Compendium::Query do
49
49
  query = described_class.new(:blank, {}, nil)
50
50
  query.run(nil).should be_empty
51
51
  end
52
+
53
+ it "should filter the result set if a filter is provided" do
54
+ query.add_filter(-> data { data.reject(&:odd?) })
55
+ query.run(nil).should == [2]
56
+ end
57
+
58
+ it "should run multiple filters if given" do
59
+ query.add_filter(-> data { data.reject(&:odd?) })
60
+ query.add_filter(-> data { data.reject(&:even?) })
61
+ query.run(nil).should == []
62
+ end
63
+
64
+ context "when the query belongs to a report class" do
65
+ let(:report) do
66
+ Class.new(Compendium::Report) do
67
+ query(:test) { [1, 2, 3] }
68
+ end
69
+ end
70
+
71
+ subject { report.queries[:test] }
72
+
73
+ before { described_class.any_instance.stub(:fetch_results) { |c| c } }
74
+
75
+ it "should return its results" do
76
+ subject.run(nil).should == [1, 2, 3]
77
+ end
78
+
79
+ it "should not affect the report" do
80
+ subject.run(nil)
81
+ report.queries[:test].results.should be_nil
82
+ end
83
+
84
+ it "should not affect future instances of the report" do
85
+ subject.run(nil)
86
+ report.new.queries[:test].results.should be_nil
87
+ end
88
+ end
52
89
  end
53
90
 
54
91
  describe "#nil?" do
@@ -65,9 +102,10 @@ describe Compendium::Query do
65
102
  let(:template) { double("Template") }
66
103
  subject { described_class.new(:test, {}, -> * {}) }
67
104
 
68
- it "should return nil if the query has no results" do
105
+ it "should initialize a new Chart presenter if the query has no results" do
69
106
  subject.stub(empty?: true)
70
- subject.render_chart(template).should be_nil
107
+ Compendium::Presenters::Chart.should_receive(:new).with(template, subject).and_return(double("Presenter").as_null_object)
108
+ subject.render_chart(template)
71
109
  end
72
110
 
73
111
  it "should initialize a new Chart presenter if the query has results" do
@@ -92,4 +130,15 @@ describe Compendium::Query do
92
130
  subject.render_table(template)
93
131
  end
94
132
  end
133
+
134
+ describe "#url" do
135
+ let(:report) { double("Report") }
136
+ subject { described_class.new(:test, {}, ->{}) }
137
+ before { subject.report = report }
138
+
139
+ it "should build a URL using its report's URL" do
140
+ report.should_receive(:url).with(query: :test)
141
+ subject.url
142
+ end
143
+ end
95
144
  end
data/spec/report_spec.rb CHANGED
@@ -28,6 +28,11 @@ describe Compendium::Report do
28
28
  its(:metrics) { should_not equal report2.metrics }
29
29
  end
30
30
 
31
+ describe ".name" do
32
+ subject { TestReport = Class.new(described_class) }
33
+ its(:name) { should == :test }
34
+ end
35
+
31
36
  describe "#run" do
32
37
  context do
33
38
  let(:report_class) do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: compendium
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Vandersluis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-04-03 00:00:00.000000000 Z
11
+ date: 2014-04-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  type: :runtime
@@ -102,6 +102,7 @@ extensions: []
102
102
  extra_rdoc_files: []
103
103
  files:
104
104
  - .gitignore
105
+ - CHANGELOG.md
105
106
  - Gemfile
106
107
  - LICENSE.txt
107
108
  - README.md
@@ -129,7 +130,6 @@ files:
129
130
  - config/locales/en.yml
130
131
  - lib/compendium.rb
131
132
  - lib/compendium/abstract_chart_provider.rb
132
- - lib/compendium/chart_provider/amcharts.rb
133
133
  - lib/compendium/collection_query.rb
134
134
  - lib/compendium/context_wrapper.rb
135
135
  - lib/compendium/dsl.rb
@@ -1,20 +0,0 @@
1
- module Compendium
2
- module ChartProvider
3
- # Uses the amcharts.rb gem to provide charting
4
- class AmCharts < Compendium::AbstractChartProvider
5
- def initialize(type, data, &setup_proc)
6
- @chart = chart_class(type).new(data, &setup_proc)
7
- end
8
-
9
- def render(template, container)
10
- template.amchart(chart, container)
11
- end
12
-
13
- private
14
-
15
- def chart_class(type)
16
- ::AmCharts::Chart.const_get(type.to_s.titlecase)
17
- end
18
- end
19
- end
20
- end