compendium 1.0.7 → 1.1.0

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